From fb39a7bb69cc55d2e1ce61d47e0677aa40540f95 Mon Sep 17 00:00:00 2001
From: chenyao <1219716595@qq.com>
Date: Fri, 30 Jan 2026 16:20:33 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
applications/drone-command/src/utils/cesium/shapeTools/edit/EditEllipseTool.js | 105
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue | 22
applications/drone-command/src/assets/images/areaMap/buffer.png | 0
applications/drone-command/src/styles/common/cockpit.scss | 9
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawRectangleTool.js | 0
applications/drone-command/src/utils/cesium/deviceRange.js | 117 +
applications/drone-command/src/components/map-container/device-map-container.vue | 424 ++-
applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue | 3
applications/drone-command/src/views/dataCockpit/components/LegendBar.vue | 33
applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js | 2
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue | 88
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js | 12
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue | 133 +
applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue | 43
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue | 153
applications/drone-command/src/assets/images/areaMap/trash.png | 0
applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue | 2
applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue | 169 +
applications/drone-command/src/views/api.txt | 148
applications/drone-command/src/utils/cesium/shapeTools/index.js | 13
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue | 8
packages/constants/areaStyles.js | 36
packages/constants/index.js | 1
applications/drone-command/env/.env.development | 4
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue | 37
applications/drone-command/src/views/audioCall.vue | 600 +++---
applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue | 1
applications/drone-command/src/utils/cesium/shapeTools/ToolBase.js | 0
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue | 439 ++--
applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js | 28
uniapps/work-app/src/subPackages/workDetail/index.vue | 36
applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js | 0
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue | 671 ------
applications/drone-command/src/views/areaManage/sceneConfig/index.vue | 13
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawEllipseTool.js | 11
applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js | 38
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue | 2
applications/drone-command/src/views/areaManage/sceneManage/index.vue | 172 +
applications/drone-command/src/views/permissionManage/permissionDept/index.vue | 39
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue | 90
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawPolygonTool.js | 0
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue | 1042 ++++++++++-
applications/drone-command/src/assets/images/areaMap/ellipse.png | 0
applications/drone-command/src/utils/cesium/shapeTools/edit/EditRectangleTool.js | 0
applications/drone-command/src/utils/cesium/shapeTools/EditManager.js | 0
applications/drone-command/src/assets/images/areaMap/polygon.png | 0
uniapps/work-app/src/pages/login/index.vue | 4
applications/drone-command/src/utils/cesium/shapeTools/Tooltip.js | 0
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue | 2
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue | 26
applications/drone-command/src/utils/cesium/shapeTools/DrawManager.js | 0
applications/drone-command/src/assets/images/areaMap/rect.png | 0
applications/drone-command/src/views/areaManage/partition/index.vue | 30
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue | 553 ++++++
applications/drone-command/src/assets/images/areaManage/positionIcon.png | 0
applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js | 45
56 files changed, 3,629 insertions(+), 1,775 deletions(-)
diff --git a/applications/drone-command/env/.env.development b/applications/drone-command/env/.env.development
index 6524616..1f6d35a 100644
--- a/applications/drone-command/env/.env.development
+++ b/applications/drone-command/env/.env.development
@@ -2,7 +2,7 @@
# @Author : yuan
# @Date : 2026-01-07 14:58:30
# @LastEditors : yuan
- # @LastEditTime : 2026-01-28 09:29:05
+ # @LastEditTime : 2026-01-30 09:29:25
# @FilePath : \applications\drone-command\env\.env.development
# @Description :
# Copyright 2026 OBKoro1, All Rights Reserved.
@@ -16,7 +16,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
-VITE_APP_URL= http://192.168.1.227
+VITE_APP_URL= http://192.168.1.168
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
diff --git a/applications/drone-command/src/assets/images/areaManage/positionIcon.png b/applications/drone-command/src/assets/images/areaManage/positionIcon.png
new file mode 100644
index 0000000..a569e90
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaManage/positionIcon.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/areaMap/buffer.png b/applications/drone-command/src/assets/images/areaMap/buffer.png
new file mode 100644
index 0000000..9c39133
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaMap/buffer.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/areaMap/ellipse.png b/applications/drone-command/src/assets/images/areaMap/ellipse.png
new file mode 100644
index 0000000..548ba94
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaMap/ellipse.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/areaMap/polygon.png b/applications/drone-command/src/assets/images/areaMap/polygon.png
new file mode 100644
index 0000000..c893971
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaMap/polygon.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/areaMap/rect.png b/applications/drone-command/src/assets/images/areaMap/rect.png
new file mode 100644
index 0000000..d00b20e
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaMap/rect.png
Binary files differ
diff --git a/applications/drone-command/src/assets/images/areaMap/trash.png b/applications/drone-command/src/assets/images/areaMap/trash.png
new file mode 100644
index 0000000..acf8c69
--- /dev/null
+++ b/applications/drone-command/src/assets/images/areaMap/trash.png
Binary files differ
diff --git a/applications/drone-command/src/components/map-container/device-map-container.vue b/applications/drone-command/src/components/map-container/device-map-container.vue
index f7fb7e9..659f8ca 100644
--- a/applications/drone-command/src/components/map-container/device-map-container.vue
+++ b/applications/drone-command/src/components/map-container/device-map-container.vue
@@ -47,9 +47,9 @@
<script setup>
import * as Cesium from 'cesium'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
-import { geomAnalysis } from '@ztzf/utils'
-import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
-import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
+import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
+import { fwAreaDivideListApi } from '@/views/areaManage/partition/partitionApi'
import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
import { cockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
@@ -61,17 +61,18 @@
import DevicePopup from './components/DevicePopup.vue'
import DronePopup from './components/DronePopup.vue'
import {
- RING_STYLES,
- addDeviceRings,
createDroneTrackMaterial,
createRadialGradientMaterial,
getTexturedVertexFormat,
} from './device-map-materials'
+import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
+import { createDeviceRangePrimitiveWithHeight } from '@/utils/cesium/deviceRange'
import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png'
import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png'
const CLUSTER_HEIGHT = 100000
const DETAIL_HEIGHT = 10000
+const POLYGON_HEIGHT_M = 0.2
const props = defineProps({
onlineDevices: {
@@ -97,7 +98,6 @@
})
const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
-const DEFAULT_ZONE_PAGE_SIZE = 999
const mapRef = ref(null)
let viewer = null
@@ -108,10 +108,8 @@
let deviceBillboardCollection = null
let deviceRingOutlinePrimitives = []
let deviceRingFillPrimitives = []
-let defenseZonePrimitive = null
-let defenseZoneOutlinePrimitive = null
-let partitionPrimitive = null
-let partitionOutlinePrimitive = null
+let partitionFillPrimitives = []
+let partitionOutlinePrimitives = []
let aggregationSource = null
let commandPostBillboardCollection = null
let droneTrackBillboardCollection = null
@@ -250,6 +248,7 @@
return range
}
+
const ensureCockpitPrimitiveLayer = () => {
if (!viewer) return
if (!cockpitPrimitiveLayer) {
@@ -280,10 +279,12 @@
if (!viewer) return
ensureCockpitPrimitiveLayer()
cockpitPrimitiveLayer.removeAll(false)
- if (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive)
- if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive)
- if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive)
- if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive)
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
if (deviceRingFillPrimitives.length) {
deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
}
@@ -323,27 +324,16 @@
}
}
-const clearDefenseZoneEntities = () => {
- if (!viewer) return
- if (defenseZonePrimitive) {
- removeCockpitPrimitive(defenseZonePrimitive)
- defenseZonePrimitive = null
- }
- if (defenseZoneOutlinePrimitive) {
- removeCockpitPrimitive(defenseZoneOutlinePrimitive)
- defenseZoneOutlinePrimitive = null
- }
-}
const clearPartitionEntities = () => {
if (!viewer) return
- if (partitionPrimitive) {
- removeCockpitPrimitive(partitionPrimitive)
- partitionPrimitive = null
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ partitionFillPrimitives = []
}
- if (partitionOutlinePrimitive) {
- removeCockpitPrimitive(partitionOutlinePrimitive)
- partitionOutlinePrimitive = null
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ partitionOutlinePrimitives = []
}
}
@@ -378,10 +368,16 @@
const setDetailVisibility = visible => {
detailVisible.value = visible
- if (defenseZonePrimitive) defenseZonePrimitive.show = visible
- if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible
- if (partitionPrimitive) partitionPrimitive.show = visible
- if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
if (deviceBillboardCollection) deviceBillboardCollection.show = visible
if (deviceRingFillPrimitives.length) {
@@ -701,7 +697,7 @@
reorderCockpitPrimitives()
}
-const renderDeviceEntities = devices => {
+const renderDeviceEntities = async devices => {
if (!viewer) return
ensureCockpitPrimitiveLayer()
ensureDeviceCollections()
@@ -716,14 +712,14 @@
}
devicePickMap.clear()
deviceBillboardCollection.show = detailVisible.value
- const ringFillInstancesByStyle = RING_STYLES.map(() => [])
- const ringOutlineInstancesByStyle = RING_STYLES.map(() => [])
+ const deviceEntries = []
+ const devicePositions = []
devices.forEach((item, index) => {
const position = getDevicePosition(item)
if (!position) return
const entityId = `online-device-${item.id ?? index}-${index}`
const rangeMeters = getDeviceRange(item)
- addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters)
+ devicePositions.push({ lng: position.longitude, lat: position.latitude })
const billboard = deviceBillboardCollection.add({
position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
image: equipmentIcon,
@@ -733,161 +729,249 @@
})
billboard.id = entityId
devicePickMap.set(entityId, { data: item, billboard })
+ deviceEntries.push({ position, rangeMeters, billboard })
})
if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
closePopup()
}
- ringFillInstancesByStyle.forEach((instances, index) => {
- if (!instances.length) return
- const ring = RING_STYLES[index]
- const alpha = ring.alpha ?? 0.64
- const innerRatio =
- typeof ring.innerRatio === 'number'
- ? ring.innerRatio
- : ring.inner && ring.outer
- ? Math.min(Math.max(ring.inner / ring.outer, 0), 0.9)
- : 0
- const material = createRadialGradientMaterial(
- Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(alpha),
- Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(alpha),
- {
- gamma: 1.7,
- innerCutoff: innerRatio,
+ if (deviceEntries.length) {
+ const heights = await getPointPositionsHeight(devicePositions, viewer)
+ deviceEntries.forEach((entry, index) => {
+ const height = Number(heights?.[index]?.ASL)
+ const centerHeight = Number.isFinite(height) ? height : 0
+ if (entry.billboard) {
+ entry.billboard.position = Cesium.Cartesian3.fromDegrees(
+ entry.position.longitude,
+ entry.position.latitude,
+ centerHeight
+ )
}
- )
- const primitive = new Cesium.GroundPrimitive({
- geometryInstances: instances,
- appearance: new Cesium.MaterialAppearance({
- material,
- translucent: true,
- }),
+ if (!Number.isFinite(entry.rangeMeters) || entry.rangeMeters <= 0) return
+ const primitive = createDeviceRangePrimitiveWithHeight(entry.position, entry.rangeMeters, centerHeight)
+ if (!primitive) return
+ primitive.show = detailVisible.value
+ addCockpitPrimitive(primitive)
+ deviceRingFillPrimitives.push(primitive)
})
- primitive.show = detailVisible.value
- addCockpitPrimitive(primitive)
- deviceRingFillPrimitives.push(primitive)
- })
- ringOutlineInstancesByStyle.forEach(instances => {
- if (!instances.length) return
- const primitive = new Cesium.GroundPolylinePrimitive({
- geometryInstances: instances,
- appearance: new Cesium.PolylineColorAppearance(),
- })
- primitive.show = detailVisible.value
- addCockpitPrimitive(primitive)
- deviceRingOutlinePrimitives.push(primitive)
- })
+ }
reorderCockpitPrimitives()
}
-const getDefenseZonePositions = geom => {
- const points = geomAnalysis(geom)
- if (points.length < 3) return []
- const first = points[0]
- const last = points[points.length - 1]
- const list =
- first && last && first.longitude === last.longitude && first.latitude === last.latitude
- ? points.slice(0, -1)
- : points
- return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
+const elevatePositions = async (positions, height = POLYGON_HEIGHT_M) => {
+ if (!Array.isArray(positions) || !positions.length) return []
+ if (!viewer) return positions
+ const points = positions
+ .map(pos => {
+ const carto = Cesium.Cartographic.fromCartesian(pos)
+ if (!carto) return null
+ return {
+ lng: Cesium.Math.toDegrees(carto.longitude),
+ lat: Cesium.Math.toDegrees(carto.latitude),
+ }
+ })
+ .filter(Boolean)
+ if (!points.length) return positions
+ const heights = await getPointPositionsHeight(points, viewer)
+ return heights.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, (item.ASL ?? 0) + height))
}
-const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => {
- if (!viewer) return { primitive: null, outlinePrimitive: null }
- const polygonInstances = []
- const lineInstances = []
- const texturedVertexFormat = getTexturedVertexFormat()
- ; (zones || []).forEach((zone, index) => {
- if (!zone?.geom) return
- const positions = getDefenseZonePositions(zone.geom)
- if (!positions.length) return
- const polygon = new Cesium.PolygonGeometry({
- polygonHierarchy: new Cesium.PolygonHierarchy(positions),
- vertexFormat: texturedVertexFormat,
+const getAreaTypeStyle = areaType => {
+ return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
+}
+
+const buildPartitionPrimitives = async shapes => {
+ if (!viewer) return { primitives: [], outlinePrimitives: [] }
+ const vertexFormat = getTexturedVertexFormat()
+ const groups = new Map()
+ const tasks = []
+ ; (shapes || []).forEach(shape => {
+ if (!shape?.positions || shape.positions.length < 3) return
+ const outlineColor = shape.outlineColor
+ const fillColor = shape.fillColor
+ const key = `${outlineColor.toCssColorString()}|${fillColor.toCssColorString()}`
+ if (!groups.has(key)) {
+ groups.set(key, { outlineColor, fillColor, polygonInstances: [], lineInstances: [] })
+ }
+ const group = groups.get(key)
+ const positions = shape.positions
+ tasks.push(
+ elevatePositions(positions).then(elevatedPositions => {
+ if (!elevatedPositions.length) return
+ const polygon = new Cesium.PolygonGeometry({
+ polygonHierarchy: new Cesium.PolygonHierarchy(elevatedPositions),
+ perPositionHeight: true,
+ vertexFormat,
+ })
+ group.polygonInstances.push(
+ new Cesium.GeometryInstance({
+ geometry: polygon,
+ })
+ )
+ const linePositions =
+ elevatedPositions.length > 1
+ ? [...elevatedPositions, elevatedPositions[0]]
+ : elevatedPositions
+ group.lineInstances.push(
+ new Cesium.GeometryInstance({
+ geometry: new Cesium.PolylineGeometry({
+ positions: linePositions,
+ width: 2,
+ }),
+ attributes: {
+ color: Cesium.ColorGeometryInstanceAttribute.fromColor(outlineColor),
+ },
+ })
+ )
+ })
+ )
+ })
+
+ if (tasks.length) await Promise.all(tasks)
+
+ const primitives = []
+ const outlinePrimitives = []
+ groups.forEach(group => {
+ if (group.polygonInstances.length) {
+ const material = createRadialGradientMaterial(group.outlineColor, group.fillColor, {
+ gamma: 1.7,
+ innerCutoff: 0,
})
- polygonInstances.push(
- new Cesium.GeometryInstance({
- geometry: polygon,
- })
- )
- const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
- lineInstances.push(
- new Cesium.GeometryInstance({
- geometry: new Cesium.GroundPolylineGeometry({
- positions: linePositions,
- width: 2,
- }),
- attributes: {
- color: Cesium.ColorGeometryInstanceAttribute.fromColor(
- Cesium.Color.fromCssColorString(lineColor)
- ),
- },
- })
- )
- })
- let primitive = null
- if (polygonInstances.length) {
- const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
- const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
- const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
- primitive = new Cesium.GroundPrimitive({
- geometryInstances: polygonInstances,
- appearance: new Cesium.MaterialAppearance({
- material,
- translucent: true,
- }),
- })
- addCockpitPrimitive(primitive)
- }
- let outlinePrimitive = null
- if (lineInstances.length) {
- outlinePrimitive = new Cesium.GroundPolylinePrimitive({
- geometryInstances: lineInstances,
- appearance: new Cesium.PolylineColorAppearance(),
- })
- addCockpitPrimitive(outlinePrimitive)
- }
- return { primitive, outlinePrimitive }
+ const primitive = new Cesium.Primitive({
+ geometryInstances: group.polygonInstances,
+ appearance: new Cesium.MaterialAppearance({
+ material,
+ translucent: true,
+ }),
+ })
+ addCockpitPrimitive(primitive)
+ primitives.push(primitive)
+ }
+ if (group.lineInstances.length) {
+ const outlinePrimitive = new Cesium.Primitive({
+ geometryInstances: group.lineInstances,
+ appearance: new Cesium.PolylineColorAppearance(),
+ })
+ addCockpitPrimitive(outlinePrimitive)
+ outlinePrimitives.push(outlinePrimitive)
+ }
+ })
+
+ return { primitives, outlinePrimitives }
}
-const renderDefenseZones = zones => {
- if (!viewer) return
- clearDefenseZoneEntities()
- const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88')
- defenseZonePrimitive = result.primitive
- defenseZoneOutlinePrimitive = result.outlinePrimitive
- if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
- if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value
- reorderCockpitPrimitives()
+const 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) {}
+ return null
}
-const renderPartitions = zones => {
+const normalizeShapePoint = point => {
+ if (!point) return null
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ return { lng: Number(lng), lat: Number(lat), height }
+}
+
+const buildShapePositions = (points = []) => {
+ const normalized = points.map(normalizeShapePoint).filter(Boolean)
+ return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height))
+}
+
+const getShapeDisplayPoints = shape => {
+ if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
+ return shape.displayPoints
+ }
+ return shape?.points || []
+}
+
+const resolvePartitionShapes = areas => {
+ const shapes = []
+ ; (areas || []).forEach(area => {
+ const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
+ extList.forEach((item, index) => {
+ const isShapePayload = item?.drawType || item?.points
+ const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson)
+ if (!parsed) return
+ const shape = {
+ id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: parsed?.drawType ?? 'polygon',
+ areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '',
+ points: Array.isArray(parsed?.points) ? parsed.points : [],
+ displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
+ meta: parsed?.meta ?? null,
+ }
+ 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 radii = shape.meta.bufferRadii
+ .map(radius => Number(radius))
+ .filter(radius => Number.isFinite(radius) && radius > 0)
+ radii.forEach((radius, levelIndex) => {
+ const positions = buildEllipsePositions(centerCartesian, radius, radius)
+ const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
+ if (positions.length >= 3) {
+ shapes.push({
+ positions,
+ fillColor: style.fill,
+ outlineColor: style.outline,
+ })
+ }
+ })
+ return
+ }
+ const positions = buildShapePositions(getShapeDisplayPoints(shape))
+ if (positions.length >= 3) {
+ const style = getAreaTypeStyle(shape.areaType)
+ shapes.push({
+ positions,
+ fillColor: style.fill,
+ outlineColor: style.outline,
+ })
+ }
+ })
+ })
+ return shapes
+}
+
+const renderPartitions = async zones => {
if (!viewer) return
clearPartitionEntities()
- const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
- partitionPrimitive = result.primitive
- partitionOutlinePrimitive = result.outlinePrimitive
- if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
- if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
- reorderCockpitPrimitives()
-}
-
-const loadDefenseZones = async () => {
- if (!viewer) return
- try {
- const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
- renderDefenseZones(res?.data?.data?.records ?? [])
- } catch (error) {
- renderDefenseZones([])
+ const shapes = resolvePartitionShapes(zones)
+ const result = await buildPartitionPrimitives(shapes)
+ partitionFillPrimitives = result.primitives
+ partitionOutlinePrimitives = result.outlinePrimitives
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => {
+ primitive.show = detailVisible.value
+ })
}
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => {
+ primitive.show = detailVisible.value
+ })
+ }
+ reorderCockpitPrimitives()
}
const loadPartitions = async () => {
if (!viewer) return
try {
- const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
- renderPartitions(res?.data?.data?.records ?? [])
+ const res = await fwAreaDivideListApi()
+ await renderPartitions(res?.data?.data ?? [])
} catch (error) {
- renderPartitions([])
+ await renderPartitions([])
}
}
@@ -1125,7 +1209,6 @@
const stage = getStageByHeight(height)
updateStageDisplay(stage)
renderDeviceEntities(props.onlineDevices)
- loadDefenseZones()
loadPartitions()
loadAggregation()
loadCommandPosts()
@@ -1153,7 +1236,6 @@
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
clearDeviceEntities()
- clearDefenseZoneEntities()
clearPartitionEntities()
clearAggregationEntities()
clearCommandPostEntities()
diff --git a/applications/drone-command/src/styles/common/cockpit.scss b/applications/drone-command/src/styles/common/cockpit.scss
index 7837a9f..74e128f 100644
--- a/applications/drone-command/src/styles/common/cockpit.scss
+++ b/applications/drone-command/src/styles/common/cockpit.scss
@@ -582,6 +582,15 @@
.el-input__wrapper {
--el-input-text-color: #161B2C !important;
}
+
+ .el-select__selection.is-near {
+ margin-left: 0;
+
+ .el-tag {
+ color: #ffffff;
+ background: #161B2C;
+ }
+ }
}
.command-select-popper.el-select__popper {
diff --git a/applications/drone-command/src/utils/cesium/deviceRange.js b/applications/drone-command/src/utils/cesium/deviceRange.js
new file mode 100644
index 0000000..3ed20e5
--- /dev/null
+++ b/applications/drone-command/src/utils/cesium/deviceRange.js
@@ -0,0 +1,117 @@
+import * as Cesium from 'cesium'
+import { getPointPositionsHeight } from './mapUtil'
+
+const DEVICE_RANGE_MATERIAL_TYPE = 'DeviceRangeEllipsoid'
+let deviceRangeMaterialRegistered = false
+
+export const registerDeviceRangeMaterial = () => {
+ if (deviceRangeMaterialRegistered || !Cesium?.Material) return
+ deviceRangeMaterialRegistered = true
+ Cesium.Material._materialCache.addMaterial(DEVICE_RANGE_MATERIAL_TYPE, {
+ fabric: {
+ type: DEVICE_RANGE_MATERIAL_TYPE,
+ uniforms: {
+ color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
+ speed: 3.0,
+ globalAlpha: 0.9
+ },
+ source: `
+ uniform vec4 color;
+ uniform float speed;
+ uniform float globalAlpha;
+
+ 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);
+
+ if (st.t < 0.5) {
+ discard;
+ }
+
+ float alphaMars3D = abs(smoothstep(0.5, 1.0, fract(-st.t - time)));
+ alphaMars3D += 0.1;
+
+ material.alpha = alphaMars3D * globalAlpha;
+ material.diffuse = color.rgb;
+
+ return material;
+ }
+ `,
+ },
+ translucent: () => true,
+ })
+}
+
+export const createDeviceRangeMaterial = () => {
+ registerDeviceRangeMaterial()
+ return Cesium.Material.fromType(DEVICE_RANGE_MATERIAL_TYPE, {
+ color: Cesium.Color.fromBytes(0, 255, 255, 155),
+ })
+}
+
+export const 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 }
+}
+
+export const normalizeDeviceRange = value => {
+ const range = Number(value)
+ if (!Number.isFinite(range) || range <= 0) return 2000
+ return range
+}
+
+export const resolveDeviceTerrainHeight = async (viewer, position) => {
+ if (!viewer || !position) return 0
+ const lng = position.lng ?? position.longitude
+ const lat = position.lat ?? position.latitude
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return 0
+ try {
+ const results = await getPointPositionsHeight([{ lng: Number(lng), lat: Number(lat) }], viewer)
+ const height = Number(results?.[0]?.ASL)
+ return Number.isFinite(height) ? height : 0
+ } catch (error) {
+ return 0
+ }
+}
+
+export const createDeviceRangePrimitiveWithHeight = (position, rangeMeters, height, material = null) => {
+ if (!position || !Number.isFinite(rangeMeters)) return null
+ const lng = position.lng ?? position.longitude
+ const lat = position.lat ?? position.latitude
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ const center = Cesium.Cartesian3.fromDegrees(Number(lng), Number(lat), Number(height) || 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: material || createDeviceRangeMaterial(),
+ closed: true,
+ translucent: true,
+ flat: true,
+ faceForward: true,
+ }),
+ asynchronous: false
+ })
+}
+
+export const createDeviceRangePrimitive = async (viewer, position, rangeMeters, material = null) => {
+ if (!viewer) return null
+ const height = await resolveDeviceTerrainHeight(viewer, position)
+ return createDeviceRangePrimitiveWithHeight(position, rangeMeters, height, material)
+}
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/DrawManager.js b/applications/drone-command/src/utils/cesium/shapeTools/DrawManager.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/DrawManager.js
rename to applications/drone-command/src/utils/cesium/shapeTools/DrawManager.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/EditManager.js b/applications/drone-command/src/utils/cesium/shapeTools/EditManager.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/EditManager.js
rename to applications/drone-command/src/utils/cesium/shapeTools/EditManager.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/ToolBase.js b/applications/drone-command/src/utils/cesium/shapeTools/ToolBase.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/ToolBase.js
rename to applications/drone-command/src/utils/cesium/shapeTools/ToolBase.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/Tooltip.js b/applications/drone-command/src/utils/cesium/shapeTools/Tooltip.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/Tooltip.js
rename to applications/drone-command/src/utils/cesium/shapeTools/Tooltip.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js
similarity index 95%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js
index ca5d4a9..937ce48 100644
--- a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js
+++ b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js
@@ -18,6 +18,9 @@
this.circleLevel1OutlineEntity = null
this.circleLevel2OutlineEntity = null
this.circleLevel3OutlineEntity = null
+ this.radiusPoint1 = null
+ this.radiusPoint2 = null
+ this.radiusPoint3 = null
this.isDrawing = false
this.drawStep = 0
this.isCompleted = false
@@ -56,6 +59,7 @@
this.radiusLevel1 = radius
this.radiusLevel2 = radius
this.radiusLevel3 = radius
+ this.radiusPoint1 = position
this.drawStep = 2
this.tooltip.show('单击设置二级缓冲圆', click.position)
return
@@ -65,6 +69,7 @@
const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
this.radiusLevel2 = Math.max(radius, this.radiusLevel1)
this.radiusLevel3 = this.radiusLevel2
+ this.radiusPoint2 = position
this.drawStep = 3
this.tooltip.show('单击设置三级缓冲圆', click.position)
return
@@ -73,6 +78,7 @@
if (this.drawStep === 3) {
const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
this.radiusLevel3 = Math.max(radius, this.radiusLevel2)
+ this.radiusPoint3 = position
this.isDrawing = false
this.drawStep = 0
this.isCompleted = true
@@ -82,6 +88,9 @@
meta: {
center: this.centerCartesian,
bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
+ controlPoints: [this.centerCartesian, this.radiusPoint1, this.radiusPoint2, this.radiusPoint3].filter(
+ Boolean
+ ),
},
})
this.tooltip.hide()
@@ -229,6 +238,9 @@
this.radiusLevel1 = 0
this.radiusLevel2 = 0
this.radiusLevel3 = 0
+ this.radiusPoint1 = null
+ this.radiusPoint2 = null
+ this.radiusPoint3 = null
this.isDrawing = false
this.drawStep = 0
this.isCompleted = false
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawEllipseTool.js
similarity index 95%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/draw/DrawEllipseTool.js
index 724288c..5f48d80 100644
--- a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js
+++ b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawEllipseTool.js
@@ -70,7 +70,16 @@
this.drawStep = 0
this.isCompleted = true
const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
- this.notify('getPolygonPositions', positions)
+ this.notify('getPolygonPositions', {
+ positions,
+ meta: {
+ center: this.centerCartesian,
+ majorPoint: this.majorPoint,
+ minorPoint: this.minorPoint,
+ semiMajor: this.semiMajor,
+ semiMinor: this.semiMinor,
+ },
+ })
this.tooltip.hide()
this.clearPreviewEntities()
}
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawPolygonTool.js b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawPolygonTool.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawPolygonTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/draw/DrawPolygonTool.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawRectangleTool.js b/applications/drone-command/src/utils/cesium/shapeTools/draw/DrawRectangleTool.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawRectangleTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/draw/DrawRectangleTool.js
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js
similarity index 88%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js
index 1808125..61e40b4 100644
--- a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js
+++ b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js
@@ -9,6 +9,26 @@
lat: point.lat ?? point.latitude,
}))
+const resolveLngLat = point => {
+ if (!point) return null
+ const lng = point.lng ?? point.longitude
+ const lat = point.lat ?? point.latitude
+ if (Number.isFinite(lng) && Number.isFinite(lat)) {
+ return { lng: Number(lng), lat: Number(lat), height: Number(point.height) || 0 }
+ }
+ return null
+}
+
+const resolveCartesian = point => {
+ if (!point) return null
+ if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
+ return new Cesium.Cartesian3(point.x, point.y, point.z)
+ }
+ const lngLat = resolveLngLat(point)
+ if (!lngLat) return null
+ return Cesium.Cartesian3.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
+}
+
export class EditBufferCircleTool extends ToolBase {
constructor(viewer) {
super(viewer)
@@ -34,10 +54,15 @@
start(data = []) {
const inputPoints = Array.isArray(data?.points) ? data.points : data
const normalized = normalizePoints(inputPoints)
- if (!normalized.length) return
- const centerLng = normalized.reduce((sum, p) => sum + p.lng, 0) / normalized.length
- const centerLat = normalized.reduce((sum, p) => sum + p.lat, 0) / normalized.length
- this.centerCartesian = Cesium.Cartesian3.fromDegrees(centerLng, centerLat)
+ const metaCenter = resolveCartesian(data?.meta?.center)
+ if (metaCenter) {
+ this.centerCartesian = metaCenter
+ } else {
+ if (!normalized.length) return
+ const centerLng = normalized.reduce((sum, p) => sum + p.lng, 0) / normalized.length
+ const centerLat = normalized.reduce((sum, p) => sum + p.lat, 0) / normalized.length
+ this.centerCartesian = Cesium.Cartesian3.fromDegrees(centerLng, centerLat)
+ }
const radii = Array.isArray(data?.meta?.bufferRadii) ? data.meta.bufferRadii : null
if (radii?.length) {
const [r1, r2, r3] = radii
@@ -124,6 +149,12 @@
meta: {
center: this.centerCartesian,
bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
+ controlPoints: [
+ this.centerCartesian,
+ this.getRadiusPoint(this.radiusLevel1),
+ this.getRadiusPoint(this.radiusLevel2),
+ this.getRadiusPoint(this.radiusLevel3),
+ ],
},
})
}
@@ -256,6 +287,12 @@
meta: {
center: this.centerCartesian,
bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
+ controlPoints: [
+ this.centerCartesian,
+ this.getRadiusPoint(this.radiusLevel1),
+ this.getRadiusPoint(this.radiusLevel2),
+ this.getRadiusPoint(this.radiusLevel3),
+ ],
},
}
}
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditEllipseTool.js
similarity index 66%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/edit/EditEllipseTool.js
index c3b2904..228eae3 100644
--- a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js
+++ b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditEllipseTool.js
@@ -24,6 +24,36 @@
lat: point.lat ?? point.latitude,
}))
+const resolveLngLat = point => {
+ if (!point) return null
+ const lng = point.lng ?? point.longitude
+ const lat = point.lat ?? point.latitude
+ if (Number.isFinite(lng) && Number.isFinite(lat)) {
+ return { lng: Number(lng), lat: Number(lat), height: Number(point.height) || 0 }
+ }
+ return null
+}
+
+const resolveCartesian = point => {
+ if (!point) return null
+ if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
+ return new Cesium.Cartesian3(point.x, point.y, point.z)
+ }
+ const lngLat = resolveLngLat(point)
+ if (!lngLat) return null
+ return Cesium.Cartesian3.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
+}
+
+const resolveCartographic = point => {
+ if (!point) return null
+ if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
+ return Cesium.Cartographic.fromCartesian(point)
+ }
+ const lngLat = resolveLngLat(point)
+ if (!lngLat) return null
+ return Cesium.Cartographic.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
+}
+
export class EditEllipseTool extends ToolBase {
constructor(viewer, options = {}) {
super(viewer)
@@ -40,22 +70,43 @@
this.style = resolveStyle(options?.style)
}
- start(points = []) {
- const normalized = normalizePoints(points)
- const bounds = getBoundsFromLngLats(normalized)
- const center = getCenterFromBounds(bounds)
- if (!center) return
- this.centerCartesian = Cesium.Cartesian3.fromRadians(center.longitude, center.latitude)
- const eastWest = getMetersBetween(
- new Cesium.Cartographic(bounds.west, center.latitude),
- new Cesium.Cartographic(bounds.east, center.latitude)
- )
- const northSouth = getMetersBetween(
- new Cesium.Cartographic(center.longitude, bounds.south),
- new Cesium.Cartographic(center.longitude, bounds.north)
- )
- this.semiMajor = Math.max(1, eastWest / 2)
- this.semiMinor = Math.max(1, northSouth / 2)
+ start(data = []) {
+ const inputPoints = Array.isArray(data?.points) ? data.points : data
+ const meta = data?.meta || null
+ const metaCenter = resolveCartesian(meta?.center)
+ if (metaCenter) {
+ this.centerCartesian = metaCenter
+ const centerCarto = Cesium.Cartographic.fromCartesian(metaCenter)
+ const majorFromMeta = Number.isFinite(meta?.semiMajor) ? Number(meta.semiMajor) : 0
+ const minorFromMeta = Number.isFinite(meta?.semiMinor) ? Number(meta.semiMinor) : 0
+ const majorPoint = resolveCartographic(meta?.majorPoint)
+ const minorPoint = resolveCartographic(meta?.minorPoint)
+ const majorDistance =
+ majorFromMeta || (majorPoint ? getMetersBetween(centerCarto, majorPoint) : 0)
+ const minorDistance =
+ minorFromMeta || (minorPoint ? getMetersBetween(centerCarto, minorPoint) : 0)
+ if (majorDistance > 0) this.semiMajor = Math.max(1, majorDistance)
+ if (minorDistance > 0) this.semiMinor = Math.max(1, minorDistance)
+ if (this.semiMinor > this.semiMajor) this.semiMinor = this.semiMajor
+ }
+
+ if (!this.centerCartesian || !this.semiMajor || !this.semiMinor) {
+ const normalized = normalizePoints(inputPoints)
+ const bounds = getBoundsFromLngLats(normalized)
+ const center = getCenterFromBounds(bounds)
+ if (!center) return
+ this.centerCartesian = Cesium.Cartesian3.fromRadians(center.longitude, center.latitude)
+ const eastWest = getMetersBetween(
+ new Cesium.Cartographic(bounds.west, center.latitude),
+ new Cesium.Cartographic(bounds.east, center.latitude)
+ )
+ const northSouth = getMetersBetween(
+ new Cesium.Cartographic(center.longitude, bounds.south),
+ new Cesium.Cartographic(center.longitude, bounds.north)
+ )
+ this.semiMajor = Math.max(1, eastWest / 2)
+ this.semiMinor = Math.max(1, northSouth / 2)
+ }
this.dataSource = new Cesium.CustomDataSource('edit-ellipse')
this.viewer.dataSources.add(this.dataSource)
@@ -113,7 +164,16 @@
this.dragType = null
this.enableMapControl()
const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
- this.notify('getPolygonPositions', positions)
+ this.notify('getPolygonPositions', {
+ positions,
+ meta: {
+ center: this.centerCartesian,
+ majorPoint: this.getAxisPoint(this.semiMajor, 0),
+ minorPoint: this.getAxisPoint(0, this.semiMinor),
+ semiMajor: this.semiMajor,
+ semiMinor: this.semiMinor,
+ },
+ })
}
createEntities() {
@@ -172,7 +232,16 @@
getPositions() {
if (!this.centerCartesian) return []
- return buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
+ return {
+ positions: buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor),
+ meta: {
+ center: this.centerCartesian,
+ majorPoint: this.getAxisPoint(this.semiMajor, 0),
+ minorPoint: this.getAxisPoint(0, this.semiMinor),
+ semiMajor: this.semiMajor,
+ semiMinor: this.semiMinor,
+ },
+ }
}
getAxisPoint(eastMeters, northMeters) {
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js
similarity index 98%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js
index c6b2a13..0e5cd6e 100644
--- a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js
+++ b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js
@@ -74,7 +74,7 @@
}
handleMouseMove(movement) {
- this.tooltip.show('拖动顶点编辑,右键删除', movement.endPosition)
+ this.tooltip.show('拖动顶点编辑,右键删除点', movement.endPosition)
if (!this.isDragging || this.draggedIndex < 0) return
const position = this.getPositionFromScreen(movement.endPosition)
if (!position) return
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditRectangleTool.js b/applications/drone-command/src/utils/cesium/shapeTools/edit/EditRectangleTool.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditRectangleTool.js
rename to applications/drone-command/src/utils/cesium/shapeTools/edit/EditRectangleTool.js
diff --git a/applications/drone-command/src/utils/cesium/shapeTools/index.js b/applications/drone-command/src/utils/cesium/shapeTools/index.js
new file mode 100644
index 0000000..89f1ecc
--- /dev/null
+++ b/applications/drone-command/src/utils/cesium/shapeTools/index.js
@@ -0,0 +1,13 @@
+export { DrawManager } from './DrawManager'
+export { EditManager } from './EditManager'
+export { buildEllipsePositions, getBoundsFromCartesians, getBoundsFromLngLats, rectanglePositionsFromBounds, getCenterFromBounds, getMetersBetween, cartesianToLocal } from './shapeUtils'
+export { DrawPolygonTool } from './draw/DrawPolygonTool'
+export { DrawRectangleTool } from './draw/DrawRectangleTool'
+export { DrawEllipseTool } from './draw/DrawEllipseTool'
+export { DrawBufferCircleTool } from './draw/DrawBufferCircleTool'
+export { EditPolygonTool } from './edit/EditPolygonTool'
+export { EditRectangleTool } from './edit/EditRectangleTool'
+export { EditEllipseTool } from './edit/EditEllipseTool'
+export { EditBufferCircleTool } from './edit/EditBufferCircleTool'
+export { ToolBase } from './ToolBase'
+export { MapTooltip as Tooltip } from './Tooltip'
diff --git a/applications/drone-command/src/views/areaManage/partition/shapeTools/shapeUtils.js b/applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js
similarity index 100%
rename from applications/drone-command/src/views/areaManage/partition/shapeTools/shapeUtils.js
rename to applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js
diff --git a/applications/drone-command/src/views/api.txt b/applications/drone-command/src/views/api.txt
index 22ac09a..5480053 100644
--- a/applications/drone-command/src/views/api.txt
+++ b/applications/drone-command/src/views/api.txt
@@ -1,98 +1,21 @@
-## 重点关注
+## 列表-不带分页
-**接口地址**:`/drone-fw/record/fwAlarmFavorite/save`
+**接口地址**:`/area/fwAreaDivide/list`
-**请求方式**:`POST`
+**请求方式**:`GET`
-**请求数据类型**:`application/json`
+**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
-**接口描述**:<p>传入fwAlarmFavorite</p>
-
-
-
-**请求示例**:
-
-
-```javascript
-{
- "alarmRecordId": 0
-}
-```
-
-
-**请求参数**:
-
-
-**请求参数**:
-
-
-| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
-| -------- | -------- | ----- | -------- | -------- | ------ |
-|fwAlarmFavorite|告警记录关注表|body|true|FwAlarmFavorite对象|FwAlarmFavorite对象|
-|  alarmRecordId|告警记录id||false|integer(int64)||
-
-
-**响应状态**:
-
-
-| 状态码 | 说明 | schema |
-| -------- | -------- | ----- |
-|200|OK|R|
-|201|Created||
-|401|Unauthorized||
-|403|Forbidden||
-|404|Not Found||
-
-
-**响应参数**:
-
-
-| 参数名称 | 参数说明 | 类型 | schema |
-| -------- | -------- | ----- |----- |
-|code|状态码|integer(int32)|integer(int32)|
-|data|承载数据|object||
-|msg|返回消息|string||
-|success|是否成功|boolean||
-
-
-**响应示例**:
-```javascript
-{
- "code": 0,
- "data": {},
- "msg": "",
- "success": true
-}
-```
-
-
-
-
-## 取消关注
-
-
-**接口地址**:`/drone-fw/record/fwAlarmFavorite/remove`
-
-
-**请求方式**:`POST`
-
-
-**请求数据类型**:`application/json`
-
-
-**响应数据类型**:`*/*`
-
-
-**接口描述**:<p>传入ids</p>
+**接口描述**:<p>传入filterSelected或sceneId或areaTypeKeys或isSetSceneManage或flyTime</p>
@@ -104,7 +27,11 @@
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
-|ids|主键集合|query|true|string||
+|areaTypeKeys|区域类型key集合(逗号分隔)|query|false|string||
+|filterSelected|是否过滤已被选择的数据(1过滤 0不过滤)|query|false|integer(int32)||
+|flyTime|时间筛选(yyyy-MM-dd HH:mm:ss)|query|false|string(date-time)||
+|isSetSceneManage|是否按场景管理过滤(1是 0或空否)|query|false|integer(int32)||
+|sceneId|场景id|query|false|integer(int64)||
**响应状态**:
@@ -112,8 +39,7 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
-|200|OK|R|
-|201|Created||
+|200|OK|R«List«FwAreaDivideVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -125,7 +51,28 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
-|data|承载数据|object||
+|data|承载数据|array|FwAreaDivideVO|
+|  areaCode|区域编码|string||
+|  areaName|区域名称|string||
+|  areaTypeKeys|区域面的类型集合逗号隔开|string||
+|  controlLevel|管控级别:日常/重点/严密|string||
+|  deviceIds|关联设备ID,逗号分隔|string||
+|  flyDateEnd|可飞行日期时间-结束|string(date-time)||
+|  flyDateStart|可飞行日期时间-开始|string(date-time)||
+|  fwAreaDivideExtList|面数据|array|FwAreaDivideExtVO|
+|    areaCode|区域编码|string||
+|    areaDivideId|关联主表区域划分ID|integer||
+|    areaTypeKey|区域类型数字字典key|string||
+|    areaTypeValue|区域类型数字字典value|string||
+|    geomJson|几何JSON|string||
+|    id|主键id|integer||
+|  id|主键id|integer(int64)||
+|  policeStationContactPerson|派出所联系人|string||
+|  policeStationContactPhone|派出所联系电话|string||
+|  policeStationId|关联派出所id|string||
+|  policeStationName|派出所名称|string||
+|  responseMechanism|响应机制内容|string||
+|  triggerCondition|触发条件|string||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -134,7 +81,34 @@
```javascript
{
"code": 0,
- "data": {},
+ "data": [
+ {
+ "areaCode": "",
+ "areaName": "",
+ "areaTypeKeys": "",
+ "controlLevel": "",
+ "deviceIds": "",
+ "flyDateEnd": "",
+ "flyDateStart": "",
+ "fwAreaDivideExtList": [
+ {
+ "areaCode": "",
+ "areaDivideId": 0,
+ "areaTypeKey": "",
+ "areaTypeValue": "",
+ "geomJson": "",
+ "id": 0
+ }
+ ],
+ "id": 0,
+ "policeStationContactPerson": "",
+ "policeStationContactPhone": "",
+ "policeStationId": "",
+ "policeStationName": "",
+ "responseMechanism": "",
+ "triggerCondition": ""
+ }
+ ],
"msg": "",
"success": true
}
diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
index cc492ca..302d14e 100644
--- a/applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
+++ b/applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
@@ -12,54 +12,6 @@
: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="formData.areaType"
- placeholder="请选择"
- @change="handleAreaTypeChange"
- >
- <el-option
- v-for="item in dictObj.areaType"
- :key="item.dictKey"
- :label="item.dictValue"
- :value="item.dictKey"
- />
- </el-select>
- </div>
- </div>
- <div class="map-toolbar">
- <button class="tool-btn" type="button" @click="handleDraw('polygon')">
- <span class="tool-icon polygon"></span>
- <span class="tool-text">多边形</span>
- </button>
- <button class="tool-btn" type="button" @click="handleDraw('rectangle')">
- <span class="tool-icon rect"></span>
- <span class="tool-text">矩形</span>
- </button>
- <button class="tool-btn" type="button" @click="handleDraw('ellipse')">
- <span class="tool-icon ellipse"></span>
- <span class="tool-text">椭圆</span>
- </button>
- <button class="tool-btn" type="button" @click="handleDraw('buffer')">
- <span class="tool-icon buffer"></span>
- <span class="tool-text">缓冲圆</span>
- </button>
- <button class="tool-btn danger" type="button" @click="handleClearShape">
- <span class="tool-icon trash"></span>
- <span class="tool-text">删除</span>
- </button>
- </div>
</div>
<div class="right-container">
@@ -180,28 +132,6 @@
</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="360">
- <el-table-column prop="areaType" label="类型" width="120">
- <template v-slot="{ row }">
- {{ getDictLabel(row.areaType, dictObj.areaType) }}
- </template>
- </el-table-column>
- <el-table-column prop="drawType" label="绘制类型" width="120">
- <template v-slot="{ row }">
- {{ drawTypeLabelMap[row.drawType] || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="操作" class-name="operation-btns">
- <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">
@@ -243,17 +173,15 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
-import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { computed, inject, nextTick, 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 } from './shapeTools/DrawManager'
-import { EditManager } from './shapeTools/EditManager'
+import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
-import { buildEllipsePositions } from './shapeTools/shapeUtils'
import * as turf from '@turf/turf'
import * as Cesium from 'cesium'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
@@ -291,39 +219,9 @@
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
-const currentShapeType = ref('polygon')
-const shapeList = ref([])
-const activeShapeId = ref(null)
-const drawTypeLabelMap = {
- polygon: '多边形',
- rectangle: '矩形',
- ellipse: '椭圆',
- buffer: '缓冲圆',
-}
-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 drawPolygonExample
let pointList = []
-let viewEntity
-let shapeDisplaySource
-const activeToolMode = ref('')
const rules = {
areaName: fieldRules(true, 50),
@@ -408,24 +306,6 @@
formData.value.flyDateEnd = flyDateRange.value[1] || ''
}
)
-watch(
- () => visible.value,
- val => {
- if (!val) {
- viewEntity && viewer?.entities?.remove(viewEntity)
- viewEntity = null
- clearActiveTool()
- hideTypePanel()
- clearShapeDisplay()
- }
- }
-)
-watch(
- () => formData.value.areaType,
- val => {
- updateActiveShapeAreaType(val)
- }
-)
// 加载详情
async function loadDetail () {
@@ -454,191 +334,14 @@
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 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 buildShapePositions (points = []) {
- const normalized = points.map(normalizeShapePoint).filter(Boolean)
- return normalized.map(point =>
- Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
- )
-}
-
-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 || readonly.value) {
- clearShapeDisplay()
- return
- }
- const dataSource = ensureShapeDisplaySource()
- if (!dataSource) return
- dataSource.entities.removeAll()
- shapeList.value.forEach(shape => {
- if (!shape?.points?.length) return
- 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
- }
-
- const positions = buildShapePositions(shape.points)
- 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 = []
- formData.value.areaSize = null
- formData.value.longitude = null
- formData.value.latitude = null
- 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 => {
+ pointList = _.cloneDeep(data).map(item => {
const val = cartesian3Convert(item, viewer)
return { ...val, lng: val.longitude, lat: val.latitude }
})
- applyPolygonMetrics(pointList)
- if (!formData.value.areaType) {
- const detectAreaType = getDetectAreaTypeKey()
- if (detectAreaType) {
- formData.value.areaType = detectAreaType
- }
- }
- 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 shapePayload = {
- id: activeShapeId.value,
- areaType: formData.value.areaType,
- drawType: currentShapeType.value,
- points: _.cloneDeep(pointList),
- meta: bufferRadii && centerLngLat
- ? {
- bufferRadii,
- center: {
- lng: centerLngLat.longitude,
- lat: centerLngLat.latitude,
- height: centerLngLat.height ?? 0,
- },
- }
- : null,
- }
- if (targetIndex === -1) {
- shapeList.value.push(shapePayload)
- } else {
- shapeList.value.splice(targetIndex, 1, shapePayload)
- }
- renderShapeList()
-}
-
-function applyPolygonMetrics(points) {
- if (!points?.length) return
const polygon = turf.polygon([
- [...points.map(item => [item.longitude, item.latitude]), [points[0].longitude, points[0].latitude]],
+ [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
])
const center = turf.centerOfMass(polygon)
const areaSqm = turf.area(polygon) // 平方米
@@ -647,152 +350,41 @@
formData.value.latitude = center.geometry.coordinates[1]
}
-function getDetectAreaTypeKey () {
- const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
- ? dictObj.value.areaType
- : dictObj?.areaType
- if (!Array.isArray(areaTypeOptions)) return ''
- const target = areaTypeOptions.find(item => item?.dictKey === '1')
- return target?.dictKey ?? ''
+// 新增模式绘制
+function addPolygon () {
+ drawPolygonExample = new DrawPolygon(viewer)
+ drawPolygonExample.initHandler(viewer)
+ drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
-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)
- }
- clearActiveTool()
-}
-
-function handleAreaTypeChange (value) {
- updateActiveShapeAreaType(value)
-}
-
-function updateActiveShapeAreaType (value) {
- if (currentShapeType.value !== 'buffer') {
- 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(formData.value.areaType) })
- 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(formData.value.areaType) })
- activeTool?.subscribe('getPolygonPositions', drawFinished)
-}
-
-function handleDraw (type) {
- if (readonly.value) return
- if (!viewer) return
- clearActiveTool()
- viewEntity && viewer?.entities?.remove(viewEntity)
- viewEntity = null
- hideTypePanel()
- renderShapeList()
- if (dialogMode.value === 'edit' && pointList.length >= 3) {
- const editPoints = _.cloneDeep(pointList)
- if (editPoints.length > 1) {
- const first = editPoints[0]
- const last = editPoints[editPoints.length - 1]
- if (first.longitude === last.longitude && first.latitude === last.latitude) {
- editPoints.pop()
- }
+// 编辑面
+function editPolygon () {
+ if (!formData.value?.geom) return
+ pointList = geomAnalysis(formData.value.geom)
+ drawPolygonExample?.destroy()
+ drawPolygonExample = new DrawPolygon(viewer)
+ let pointList1 = _.cloneDeep(pointList)
+ pointList1.pop()
+ drawPolygonExample.initPolygon(
+ viewer,
+ pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
+ )
+ drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
+ if (pointList1.length) {
+ const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
+ const tempEntity = viewer.entities?.add({
+ position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
+ polyline: {
+ positions: Cesium.Cartesian3.fromDegreesArray(result),
+ clampToGround: true,
+ width: 1,
+ material: Cesium.Color.TRANSPARENT,
+ },
+ })
+ if (tempEntity) {
+ viewer.flyTo(tempEntity, { duration: 0 })
+ viewer.entities.remove(tempEntity)
}
- activeShapeId.value = activeShapeId.value || (shapeList.value[0]?.id ?? null)
- startEdit(type, editPoints)
- return
- }
- const defaultAreaType = getDetectAreaTypeKey()
- if (defaultAreaType) {
- formData.value.areaType = 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
- formData.value.areaSize = null
- formData.value.longitude = null
- formData.value.latitude = null
- 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
- formData.value.areaType = row.areaType
- pointList = _.cloneDeep(row.points)
- clearActiveTool()
- const editPayload = row.drawType === 'buffer' ? { points: row.points, meta: row.meta } : row.points
- startEdit(row.drawType, editPayload)
- applyPolygonMetrics(pointList)
- 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 = []
- formData.value.areaSize = null
- formData.value.longitude = null
- formData.value.latitude = null
- hideTypePanel()
}
}
@@ -801,7 +393,7 @@
if (!formData.value?.geom) return
pointList = geomAnalysis(formData.value.geom)
const result = pointList.map(item => [item.longitude, item.latitude]).flat()
- viewEntity = viewer.entities?.add({
+ const mian = viewer.entities?.add({
customType: 'control_group',
position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
polyline: {
@@ -811,7 +403,7 @@
material: Cesium.Color.RED,
},
})
- viewer.flyTo(viewEntity, { duration: 0 })
+ viewer.flyTo(mian, { duration: 0 })
}
// 获取派出所列表
@@ -857,32 +449,14 @@
dialogMode.value = mode || 'add'
formData.value = dialogMode.value === 'add' ? initForm() : row
selectedDeviceRows.value = []
- shapeList.value = []
- activeShapeId.value = null
await nextTick()
initMap()
- clearShapeDisplay()
- viewEntity && viewer?.entities?.remove(viewEntity)
- viewEntity = null
- clearActiveTool()
await getDeviceList()
if (dialogMode.value === 'add') {
- // default no draw mode
+ addPolygon()
} else if (dialogMode.value === 'edit') {
await loadDetail()
- pointList = geomAnalysis(formData.value.geom) || []
- if (pointList.length) {
- activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
- shapeList.value = [
- {
- id: activeShapeId.value,
- areaType: formData.value.areaType,
- drawType: currentShapeType.value,
- points: _.cloneDeep(pointList),
- },
- ]
- }
- viewPolygon()
+ editPolygon()
} else {
await loadDetail()
viewPolygon()
@@ -896,170 +470,9 @@
getDeviceList()
})
-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 {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6px;
- width: 64px;
- padding: 8px 6px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.18);
- border-radius: 6px;
- color: #ffffff;
- font-size: 12px;
- line-height: 1;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.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;
- width: 20px;
- height: 20px;
- border: 2px solid #ffffff;
- border-radius: 3px;
-}
-
-.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-top: 8px;
- padding: 8px;
- border-radius: 8px;
- background: rgba(27, 34, 56, 0.6);
-}
-
-.shape-table-title {
- margin-bottom: 8px;
- color: #e5e7f0;
- font-size: 13px;
-}
-</style>
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
index 302d14e..92c6ef8 100644
--- a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,24 +1,62 @@
<template>
- <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
- :close-on-click-modal="false">
+ <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false" :close-on-click-modal="false">
<div class="dialog-container">
<div class="left-container">
- <CommonCesiumMap
- ref="mapRef"
- class="leftMap command-cesium"
- :active="visible"
- :flat-mode="false"
- :terrain="true"
- :layer-mode="4"
- :boundary="false"
- />
+ <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>
+
+ <el-icon class="close-btn" @click.stop="visible = false">
+ <Close />
+ </el-icon>
</div>
<div class="content" v-if="readonly">
@@ -27,18 +65,6 @@
<el-col :span="24">
<div class="label">区域名称</div>
<div class="val">{{ formData.areaName }}</div>
- </el-col>
- <el-col :span="24">
- <div class="label">区域位置</div>
- <div class="val">{{ formatLocation(formData) }}</div>
- </el-col>
- <el-col :span="24">
- <div class="label">区域面积</div>
- <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
- </el-col>
- <el-col :span="24">
- <div class="label">区域类型</div>
- <div class="val">{{ getDictLabel(formData.areaType, dictObj.areaType) }}</div>
</el-col>
<el-col :span="24">
<div class="label">触发条件</div>
@@ -64,11 +90,29 @@
</el-col>
</el-row>
+ <div class="detail-title">绘制区域列表</div>
+ <div class="command-table-container">
+ <div class="command-table-content">
+ <el-table class="command-table" :data="shapeList" row-key="id">
+ <el-table-column prop="areaType" label="区域类型">
+ <template v-slot="{ row }">
+ {{ resolveShapeAreaTypeLabel(row) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="drawType" label="绘制类型">
+ <template v-slot="{ row }">
+ {{ drawTypeLabelMap[row.drawType] || '-' }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
<div class="detail-title">关联设备</div>
<div class="command-table-container">
<div class="command-table-content">
- <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions"
- row-key="id">
+ <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
+ @row-click="handleDeviceRowClick">
<el-table-column prop="deviceName" label="设备名称" />
<el-table-column prop="deviceType" label="类型">
<template v-slot="{ row }">
@@ -83,25 +127,12 @@
<el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
label-width="96px">
<el-form-item label="区域名称" prop="areaName">
- <el-input class="command-input" v-model="formData.areaName" maxlength="50"
- placeholder="请输入" clearable />
- </el-form-item>
- <el-form-item label="区域位置" prop="longitude">
- <div class="val">{{ formatLocation(formData) }}</div>
- </el-form-item>
- <el-form-item label="区域面积" prop="areaSize">
- <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
- </el-form-item>
- <el-form-item label="区域类型" prop="areaType">
- <el-select class="command-select" popper-class="command-select-popper"
- v-model="formData.areaType" placeholder="请选择" clearable>
- <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
- :value="item.dictKey" />
- </el-select>
+ <el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
+ clearable />
</el-form-item>
<el-form-item label="响应机制" prop="responseMechanism">
- <el-input class="command-input" v-model="formData.responseMechanism"
- maxlength="200" placeholder="请输入" clearable />
+ <el-input class="command-input" v-model="formData.responseMechanism" maxlength="200"
+ placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="触发条件" prop="triggerCondition">
<el-select class="command-select" popper-class="command-select-popper"
@@ -118,10 +149,9 @@
</el-select>
</el-form-item>
<el-form-item label="可飞行时段" prop="flyDateStart">
- <el-date-picker class="command-date-picker"
- popper-class="command-date-picker-popper" v-model="flyDateRange"
- type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
- :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
+ <el-date-picker class="command-date-picker" popper-class="command-date-picker-popper"
+ v-model="flyDateRange" type="datetimerange" range-separator="至" start-placeholder="开始时间"
+ end-placeholder="结束时间" :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
value-format="YYYY-MM-DD HH:mm:ss" clearable />
</el-form-item>
<el-form-item label="关联派出所" prop="policeStationId">
@@ -132,20 +162,41 @@
</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>
+ <el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
</div>
<el-form-item prop="deviceIds" label-width="0">
<el-table class="command-table" ref="deviceTableRef"
:data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id"
- @selection-change="handleDeviceSelectionChange">
+ @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick">
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="deviceName" label="设备名称" />
<el-table-column prop="deviceType" label="类型">
@@ -159,7 +210,9 @@
</el-form>
<div class="footer">
- <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
+ <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' :
+ '取消'
+ }}</el-button>
<el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
:disabled="submitting" @click="handleSubmit">
确定
@@ -173,30 +226,37 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
-import { computed, inject, nextTick, onMounted, ref, watch } from '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 { DrawPolygon } from '@/utils/cesium/DrawPolygon'
+import { DrawManager, EditManager, buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
-import * as turf from '@turf/turf'
+import {
+ createDeviceRangePrimitive,
+ getDeviceLngLat,
+ normalizeDeviceRange,
+ resolveDeviceTerrainHeight,
+} from '@/utils/cesium/deviceRange'
import * as Cesium from 'cesium'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
+import 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: '', // 区域名称
- areaSize: null, // 区域面积
- areaType: '', // 区域类型
controlLevel: '', // 管控级别
deviceIds: '', // 关联设备ID
flyDateEnd: '', // 可飞行结束
flyDateStart: '', // 可飞行开始
- latitude: null, // 区域中心纬度
- longitude: null, // 区域中心经度
policeStationId: '', // 关联派出所id
responseMechanism: '', // 响应机制
triggerCondition: '', // 触发条件
@@ -219,14 +279,31 @@
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
+const currentShapeType = ref('polygon')
+const activeAreaType = ref('')
+const shapeList = ref([])
+const activeShapeId = ref(null)
+const drawTypeLabelMap = {
+ polygon: '多边形',
+ rectangle: '矩形',
+ ellipse: '椭圆',
+ buffer: '缓冲圆',
+}
+const bufferAreaTypeValue = '1,2,3'
+const showTypePanel = ref(false)
+const suppressTypePanelOnce = ref(false)
let viewer
-let drawPolygonExample
+let drawManager
+let editManager
+let activeTool
let pointList = []
+let viewEntity
+let shapeDisplaySource
+const deviceRangePrimitiveMap = new Map()
+const activeToolMode = ref('')
const rules = {
areaName: fieldRules(true, 50),
- longitude: fieldRules(true),
- areaType: fieldRules(true),
responseMechanism: fieldRules(true, 200),
triggerCondition: fieldRules(true, 200),
controlLevel: fieldRules(true),
@@ -274,17 +351,33 @@
// 提交新增/编辑
async function handleSubmit () {
- const isValid = await formRef.value?.validate().catch(() => false)
- if (!isValid) return
- if (dialogMode.value === 'add' && Number(formData.value.areaSize) < 0.5) {
- ElMessage.warning('区域面积不能小于0.5km²,请重新绘制')
+ if (!shapeList.value.length) {
+ ElMessage.warning('区域不可为空,请先绘制区域')
return
}
+
+ const isValid = await formRef.value?.validate().catch(() => false)
+ if (!isValid) return
submitting.value = true
try {
- let str = [...pointList, pointList[0]].map(item => `${item.longitude} ${item.latitude}`).join(',')
- formData.value.geom = `POLYGON((${str}))`
- await fwAreaDivideSubmitApi(formData.value)
+ const areaDivide = { ...formData.value }
+ delete areaDivide.geom
+ const areaDivideExtList = shapeList.value.map(shape => {
+ const areaTypeKey = shape?.areaType ?? ''
+ const areaTypeValue = getDictLabel(areaTypeKey, resolveAreaTypeOptions())
+ return {
+ id: shape?.extId,
+ areaDivideId: areaDivide?.id ?? '',
+ areaCode: areaDivide?.areaCode ?? '',
+ areaTypeKey,
+ areaTypeValue,
+ geomJson: JSON.stringify(shape),
+ }
+ })
+ await fwAreaDivideSubmitApi({
+ areaDivide,
+ areaDivideExtList,
+ })
const actionText = dialogMode.value === 'add' ? '新增' : '编辑'
saveOperationLog({
requestUri: route.path,
@@ -306,19 +399,43 @@
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 })
- formData.value = res?.data?.data ?? initForm()
+ const detail = res?.data?.data ?? {}
+ const nextForm = initForm()
+ Object.keys(nextForm).forEach(key => {
+ if (detail[key] !== undefined) {
+ nextForm[key] = detail[key]
+ }
+ })
+ if (detail?.id) {
+ nextForm.id = detail.id
+ }
+ formData.value = nextForm
flyDateRange.value = [formData.value.flyDateStart, formData.value.flyDateEnd].filter(Boolean)
-}
-
-// 格式化区域位置
-function formatLocation (row) {
- if (row?.longitude == null || row?.latitude == null) return ''
- return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
+ syncShapeListFromDetail(detail)
}
// 格式化可飞行时间段
@@ -334,66 +451,531 @@
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()
+}
+
+async function renderDeviceRangeSingle (device) {
+ if (!viewer) return
+ clearDeviceRangePrimitives()
+ const position = getDeviceLngLat(device)
+ if (!position) return
+ 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
+ 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)
+ }
+ }
+ 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 }
+ const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
+ const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
+ return { row, primitive }
+ })
+ )
+ primitives.forEach(({ row, primitive }) => {
+ if (!primitive) return
+ const id = String(row?.id)
+ 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 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,
+ },
+ })
+ })
+ }
+ 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 => {
- pointList = _.cloneDeep(data).map(item => {
+ const positions = Array.isArray(data?.positions) ? data.positions : data
+ if (!Array.isArray(positions) || positions.length < 3) {
+ pointList = []
+ hideTypePanel()
+ return
+ }
+ const meta = data?.meta
+ const bufferRadii = Array.isArray(meta?.bufferRadii) ? meta.bufferRadii : null
+ const centerCartesian = meta?.center
+ const centerLngLat = centerCartesian ? cartesian3Convert(centerCartesian, viewer) : null
+ pointList = _.cloneDeep(positions).map(item => {
const val = cartesian3Convert(item, viewer)
return { ...val, lng: val.longitude, lat: val.latitude }
})
- const polygon = turf.polygon([
- [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
- ])
- const center = turf.centerOfMass(polygon)
- const areaSqm = turf.area(polygon) // 平方米
- formData.value.areaSize = _.round(areaSqm / 1_000_000, 2) // 平方千米
- formData.value.longitude = center.geometry.coordinates[0]
- formData.value.latitude = center.geometry.coordinates[1]
-}
-
-// 新增模式绘制
-function addPolygon () {
- drawPolygonExample = new DrawPolygon(viewer)
- drawPolygonExample.initHandler(viewer)
- drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
-}
-
-// 编辑面
-function editPolygon () {
- if (!formData.value?.geom) return
- pointList = geomAnalysis(formData.value.geom)
- drawPolygonExample?.destroy()
- drawPolygonExample = new DrawPolygon(viewer)
- let pointList1 = _.cloneDeep(pointList)
- pointList1.pop()
- drawPolygonExample.initPolygon(
- viewer,
- pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
- )
- drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
- if (pointList1.length) {
- const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
- const tempEntity = viewer.entities?.add({
- position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
- polyline: {
- positions: Cesium.Cartesian3.fromDegreesArray(result),
- clampToGround: true,
- width: 1,
- material: Cesium.Color.TRANSPARENT,
- },
- })
- if (tempEntity) {
- viewer.flyTo(tempEntity, { duration: 0 })
- viewer.entities.remove(tempEntity)
+ const controlPoints = resolveControlPoints(meta, currentShapeType.value)
+ if (!activeAreaType.value) {
+ const detectAreaType = getDetectAreaTypeKey()
+ if (detectAreaType) {
+ activeAreaType.value = detectAreaType
}
+ }
+ if (currentShapeType.value === 'buffer') {
+ activeAreaType.value = bufferAreaTypeValue.split(',')
+ }
+ if (!suppressTypePanelOnce.value) {
+ showTypePanelAtCurrent()
+ }
+ suppressTypePanelOnce.value = false
+
+ if (!activeShapeId.value) {
+ activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
+ }
+ const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
+ const displayPoints = ['ellipse', 'buffer'].includes(currentShapeType.value)
+ ? _.cloneDeep(pointList)
+ : null
+ let shapeMeta = null
+ if (currentShapeType.value === 'buffer' && bufferRadii?.length && centerLngLat) {
+ shapeMeta = {
+ bufferRadii,
+ center: { lng: centerLngLat.longitude, lat: centerLngLat.latitude, height: centerLngLat.height ?? 0 },
+ }
+ if (controlPoints.length) {
+ shapeMeta.controlPoints = controlPoints
+ }
+ } else if (currentShapeType.value === 'ellipse') {
+ const centerPoint = resolveLngLatPoint(meta?.center)
+ const majorPoint = resolveLngLatPoint(meta?.majorPoint)
+ const minorPoint = resolveLngLatPoint(meta?.minorPoint)
+ if (centerPoint || majorPoint || minorPoint) {
+ shapeMeta = {
+ center: centerPoint,
+ majorPoint,
+ minorPoint,
+ semiMajor: Number.isFinite(Number(meta?.semiMajor)) ? Number(meta.semiMajor) : null,
+ semiMinor: Number.isFinite(Number(meta?.semiMinor)) ? Number(meta.semiMinor) : null,
+ }
+ if (controlPoints.length) {
+ shapeMeta.controlPoints = controlPoints
+ }
+ }
+ }
+ const shapePayload = {
+ id: activeShapeId.value,
+ areaType: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
+ drawType: currentShapeType.value,
+ points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
+ ? controlPoints
+ : _.cloneDeep(pointList),
+ displayPoints,
+ meta: shapeMeta,
+ }
+ if (targetIndex === -1) {
+ shapeList.value.push(shapePayload)
+ } else {
+ shapeList.value.splice(targetIndex, 1, shapePayload)
+ }
+ renderShapeList()
+}
+
+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()
- const mian = viewer.entities?.add({
+ viewEntity = viewer.entities?.add({
customType: 'control_group',
position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
polyline: {
@@ -403,7 +985,7 @@
material: Cesium.Color.RED,
},
})
- viewer.flyTo(mian, { duration: 0 })
+ viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
@@ -427,21 +1009,42 @@
selectedDeviceRows.value = rows
const ids = rows.map(item => item.id)
formData.value.deviceIds = ids.join(',')
+ renderDeviceRanges(rows)
+ const target = rows[rows.length - 1]
+ if (target) {
+ void flyToDevice(target)
+ }
+}
+
+function handleDeviceRowClick (row) {
+ if (readonly.value) {
+ renderDeviceRangeSingle(row)
+ void flyToDevice(row)
+ return
+ }
+ renderDeviceRanges(selectedDeviceRows.value)
+ void flyToDevice(row)
}
// 同步选择状态
function syncDeviceSelection () {
- if (!deviceTableRef.value) return
- deviceTableRef.value.clearSelection()
const rows = []
const arr = formData.value.deviceIds.split(',')
deviceOptions.value.forEach(row => {
if (arr.includes(row.id)) {
- deviceTableRef.value.toggleRowSelection(row, true)
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)
+ })
}
// 打开弹框
@@ -449,25 +1052,40 @@
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') {
- addPolygon()
+ // default no draw mode
} else if (dialogMode.value === 'edit') {
await loadDetail()
- editPolygon()
+ viewPolygon()
} else {
await loadDetail()
viewPolygon()
}
await nextTick()
syncDeviceSelection()
+ if (!readonly.value) {
+ renderDeviceRanges(selectedDeviceRows.value)
+ }
}
onMounted(() => {
getPoliceStationList()
- getDeviceList()
+})
+
+onBeforeUnmount(() => {
+ hideTypePanel()
})
defineExpose({
@@ -475,4 +1093,168 @@
})
</script>
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.left-container {
+ position: relative;
+}
+
+.shape-type-panel {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ z-index: 6;
+ min-width: 260px;
+ background: rgba(22, 33, 44, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 8px;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(6px);
+ color: #e5e7f0;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.panel-close {
+ cursor: pointer;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.panel-body {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px 12px;
+}
+
+.panel-label {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.75);
+ white-space: nowrap;
+}
+
+.map-toolbar {
+ position: absolute;
+ top: 64px;
+ right: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 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>
diff --git a/applications/drone-command/src/views/areaManage/partition/index.vue b/applications/drone-command/src/views/areaManage/partition/index.vue
index bd3f8ae..9f81fd7 100644
--- a/applications/drone-command/src/views/areaManage/partition/index.vue
+++ b/applications/drone-command/src/views/areaManage/partition/index.vue
@@ -5,10 +5,9 @@
<el-input class="command-input" v-model="searchParams.areaName" placeholder="请输入"
clearable @clear="handleSearch" />
</el-form-item>
-
- <el-form-item label="区域类型" prop="areaType">
+ <el-form-item label="区域类型" prop="areaTypeKeys">
<el-select class="command-select" popper-class="command-select-popper"
- v-model="searchParams.areaType" placeholder="请选择" clearable @change="handleSearch">
+ v-model="searchParams.areaTypeKeys" placeholder="请选择" clearable @change="handleSearch">
<el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
:value="item.dictKey" />
</el-select>
@@ -31,30 +30,24 @@
<el-table-column type="selection" width="46" />
<el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
<el-table-column prop="areaName" show-overflow-tooltip width="150" label="区域名称" />
- <el-table-column show-overflow-tooltip width="160" label="区域位置">
+ <el-table-column prop="areaTypeKeys" show-overflow-tooltip width="196" label="区域类型">
<template v-slot="{ row }">
- {{ formatLocation(row) }}
+ {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
</template>
</el-table-column>
- <el-table-column prop="areaSize" show-overflow-tooltip width="130" label="区域面积(k㎡)" />
- <el-table-column prop="areaType" show-overflow-tooltip width="120" label="区域类型">
- <template v-slot="{ row }">
- {{ getDictLabel(row.areaType, dictObj.areaType) }}
- </template>
- </el-table-column>
- <el-table-column prop="triggerCondition" show-overflow-tooltip width="130" label="触发条件">
+ <el-table-column prop="triggerCondition" show-overflow-tooltip label="触发条件">
<template v-slot="{ row }">
{{ getDictLabel(row.triggerCondition, dictObj.triggeringCondition) }}
</template>
</el-table-column>
- <el-table-column prop="responseMechanism" show-overflow-tooltip width="130" label="响应机制" />
- <el-table-column prop="controlLevel" show-overflow-tooltip width="120" label="管控级别">
+ <el-table-column prop="responseMechanism" show-overflow-tooltip label="响应机制" />
+ <el-table-column prop="controlLevel" show-overflow-tooltip label="管控级别">
<template v-slot="{ row }">
{{ getDictLabel(row.controlLevel, dictObj.controlLevel) }}
</template>
</el-table-column>
<el-table-column prop="policeStationName" show-overflow-tooltip label="关联派出所" />
- <el-table-column show-overflow-tooltip label="可飞行时间段">
+ <el-table-column show-overflow-tooltip width="300" label="可飞行时间段">
<template v-slot="{ row }">
{{ formatFlyDate(row) }}
</template>
@@ -93,7 +86,7 @@
const initSearchParams = () => ({
areaName: '', // 区域名称
- areaType: '', // 区域类型
+ areaTypeKeys: '', // 区域类型
current: 1, // 当前页
size: 10, // 每页大小
})
@@ -157,11 +150,6 @@
// 勾选值设置
function handleSelectionChange (rows) {
selectedIds.value = rows.map(item => item.id)
-}
-
-function formatLocation (row) {
- if (row?.longitude == null || row?.latitude == null) return ''
- return `${row.longitude}, ${row.latitude}`
}
function formatFlyDate (row) {
diff --git a/applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
index 064f7ad..50e15ee 100644
--- a/applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
@@ -120,6 +120,7 @@
import axios from 'axios'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
+import positionIcon from '@/assets/images/areaManage/positionIcon.png'
const initForm = () => ({
address: '', // 位置
@@ -185,7 +186,7 @@
let redPointEntity
let leftClickBound = false
const labelParams = {
- font: '16px',
+ font: '18px',
fillColor: Cesium.Color.WHITE, // 文字颜色:白色
backgroundColor: Cesium.Color.BLACK, //背景颜色
backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
@@ -200,17 +201,17 @@
}
function LeftClickEvent(click) {
- const pos = click.position // Cartesian2 屏幕坐标
- // 屏幕坐标 -> 椭球面坐标(不考虑地形/模型)
- const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
- if (!cartesian3) {
- return { longitude: 112, latitude: 23 }
+ const pos = click.position
+ // 优先使用 pickPosition 获取贴地坐标(包含地形高度)
+ let cartesian3 = viewer.scene.pickPosition(pos)
+ // 如果 pickPosition 失败,回退到 pickEllipsoid
+ if (!cartesian3 || !Cesium.defined(cartesian3)) {
+ cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
}
- // 椭球面坐标 -> 经纬度
+ if (!cartesian3) return
const carto = Cesium.Cartographic.fromCartesian(cartesian3)
const longitude = Cesium.Math.toDegrees(carto.longitude)
const latitude = Cesium.Math.toDegrees(carto.latitude)
- // 直接更新 formData
formData.value.longitude = _.round(longitude, 6)
formData.value.latitude = _.round(latitude, 6)
getLocationName(longitude, latitude)
@@ -219,14 +220,16 @@
if (!redPointEntity) {
redPointEntity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
- point: {
- color: Cesium.Color.RED,
- pixelSize: 10,
+ billboard: {
+ image: positionIcon,
+ width: 20,
+ height: 20,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
label: {
text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
- ...labelParams
+ ...labelParams,
},
})
} else {
@@ -240,14 +243,16 @@
if (!redPointEntity) {
redPointEntity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
- point: {
- color: Cesium.Color.RED,
- pixelSize: 10,
+ billboard: {
+ image: positionIcon,
+ width: 20,
+ height: 20,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
label: {
text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
- ...labelParams
+ ...labelParams,
},
})
} else {
diff --git a/applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
index e546c27..43d3a82 100644
--- a/applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -15,7 +15,7 @@
<div class="right-container">
<div class="header">
<span>{{ titleEnum[dialogMode] }}</span>
-
+
<el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
</div>
@@ -23,32 +23,16 @@
<div class="detail-title">场景详情</div>
<el-row>
<el-col :span="24">
- <div class="label">场景名称</div>
+ <div class="label">场景配置名称</div>
<div class="val">{{ formData.sceneName }}</div>
- </el-col>
- <el-col :span="24">
- <div class="label">指挥点位置</div>
- <div class="val">{{ formatLocation(formData) }}</div>
</el-col>
<el-col :span="24">
<div class="label">场景类型</div>
<div class="val">{{ getDictLabel(formData.sceneType, dictObj.CommandSceneType) }}</div>
</el-col>
<el-col :span="24">
- <div class="label">负责人</div>
- <div class="val">{{ formData.defenseLeader }}</div>
- </el-col>
- <el-col :span="24">
- <div class="label">负责人电话</div>
- <div class="val">{{ formData.leaderPhone }}</div>
- </el-col>
- <el-col :span="24">
<div class="label">设备模式</div>
<div class="val">{{ getDictLabel(formData.deviceMode, dictObj.deviceMode) }}</div>
- </el-col>
- <el-col :span="24">
- <div class="label">防控面积</div>
- <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
</el-col>
</el-row>
@@ -57,9 +41,9 @@
<div class="command-table-content">
<el-table class="command-table" ref="areaTableRef" :data="areaList" row-key="id">
<el-table-column prop="areaName" label="区域名称" />
- <el-table-column prop="areaType" label="区域类型">
+ <el-table-column prop="areaTypeKeys" label="区域类型">
<template v-slot="{ row }">
- {{ getDictLabel(row.areaType, dictObj.areaType) }}
+ {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
</template>
</el-table-column>
</el-table>
@@ -67,60 +51,77 @@
</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="sceneName">
- <el-input class="command-input" v-model="formData.sceneName" maxlength="50"
- placeholder="请输入" clearable />
- </el-form-item>
- <el-form-item label="指挥点位置" prop="longitude">
- <div class="val">{{ formatLocation(formData) }}</div>
+ <el-form
+ class="dialog-form"
+ v-else
+ ref="formRef"
+ :model="formData"
+ :rules="rules"
+ :disabled="readonly"
+ label-width="96px"
+ >
+ <el-form-item label="场景配置名称" prop="sceneName">
+ <el-input
+ class="command-input"
+ v-model="formData.sceneName"
+ maxlength="50"
+ placeholder="请输入"
+ clearable
+ />
</el-form-item>
<el-form-item label="场景类型" prop="sceneType">
- <el-select class="command-select" popper-class="command-select-popper"
- v-model="formData.sceneType" placeholder="请选择" clearable>
- <el-option v-for="item in dictObj.CommandSceneType" :key="item.dictKey" :label="item.dictValue"
- :value="item.dictKey" />
+ <el-select
+ class="command-select"
+ popper-class="command-select-popper"
+ v-model="formData.sceneType"
+ placeholder="请选择"
+ clearable
+ >
+ <el-option
+ v-for="item in dictObj.CommandSceneType"
+ :key="item.dictKey"
+ :label="item.dictValue"
+ :value="item.dictKey"
+ />
</el-select>
- </el-form-item>
- <el-form-item label="负责人" prop="defenseLeader">
- <el-input class="command-input" v-model="formData.defenseLeader" maxlength="50"
- placeholder="请输入" clearable />
- </el-form-item>
- <el-form-item label="负责人电话" prop="leaderPhone">
- <el-input class="command-input" v-model="formData.leaderPhone" maxlength="50"
- placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="设备模式" prop="deviceMode">
- <el-select class="command-select" popper-class="command-select-popper"
- v-model="formData.deviceMode" placeholder="请选择" clearable>
- <el-option v-for="item in dictObj.deviceMode" :key="item.dictKey" :label="item.dictValue"
- :value="item.dictKey" />
+ <el-select
+ class="command-select"
+ popper-class="command-select-popper"
+ v-model="formData.deviceMode"
+ placeholder="请选择"
+ clearable
+ >
+ <el-option
+ v-for="item in dictObj.deviceMode"
+ :key="item.dictKey"
+ :label="item.dictValue"
+ :value="item.dictKey"
+ />
</el-select>
- </el-form-item>
- <el-form-item label="防控面积" prop="defenseArea">
- <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
</el-form-item>
<div class="search-table-container">
<div class="search-box">
- <div class="label">
- 关联区域
- </div>
+ <div class="label">关联区域</div>
- <el-input class="command-input" v-model="searchName" placeholder="请输入"
- clearable></el-input>
+ <el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
</div>
<el-form-item prop="areaDivideIds" label-width="0">
- <el-table class="command-table" ref="areaTableRef"
- :data="areaList.filter(item => item.areaName.includes(searchName))" row-key="id"
- @selection-change="handleAreaSelectionChange">
+ <el-table
+ class="command-table"
+ ref="areaTableRef"
+ :data="areaList.filter(item => item.areaName.includes(searchName))"
+ row-key="id"
+ @selection-change="handleAreaSelectionChange"
+ >
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="areaName" show-overflow-tooltip label="区域名称" />
- <el-table-column prop="areaType" show-overflow-tooltip label="区域类型">
+ <el-table-column prop="areaTypeKeys" show-overflow-tooltip label="区域类型">
<template v-slot="{ row }">
- {{ getDictLabel(row.areaType, dictObj.areaType) }}
+ {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
</template>
</el-table-column>
</el-table>
@@ -129,9 +130,17 @@
</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 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>
@@ -143,29 +152,24 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
-import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
+import { computed, inject, nextTick, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDefenseSceneDetailApi, fwDefenseSceneSubmitApi } from './sceneConfigApi'
-import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
+import { fieldRules, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
-import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
-import commandPost from '@/assets/images/dataCockpit/legend/command-post.png'
+import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
const initForm = () => ({
- sceneName: '', // 场景名称
+ sceneName: '', // 场景配置名称
sceneType: '', // 场景类型
- defenseLeader: '', // 负责人
- leaderPhone: '', // 负责人电话
deviceMode: '', // 设备模式
areaDivideIds: '', // 关联区域ID
areaCount: 0, // 区域数量
- defenseArea: 0, // 防控面积
- latitude: null, // 指挥点纬度
- longitude: null, // 指挥点经度
})
const emit = defineEmits(['success'])
@@ -184,26 +188,22 @@
const mapRef = ref(null)
const route = useRoute()
let viewer
-let redPointEntity
-let leftClickBound = false
+let areaDisplaySource
const rules = {
sceneName: fieldRules(true, 50),
sceneType: fieldRules(true),
- defenseLeader: fieldRules(true, 50),
- leaderPhone: fieldRules(true, 50),
deviceMode: fieldRules(true),
- longitude: fieldRules(true),
areaDivideIds: fieldRules(false),
}
// 关闭弹框
-function handleCancel () {
+function handleCancel() {
visible.value = false
}
// 提交新增/编辑
-async function handleSubmit () {
+async function handleSubmit() {
const isValid = await formRef.value?.validate().catch(() => false)
if (!isValid) return
submitting.value = true
@@ -211,7 +211,6 @@
const ids = selectedAreaRows.value.map(item => item.id)
formData.value.areaDivideIds = ids.join(',')
formData.value.areaCount = ids.length
- formData.value.defenseArea = calcDefenseArea(selectedAreaRows.value)
await fwDefenseSceneSubmitApi(formData.value)
saveOperationLog({
requestUri: route.path,
@@ -227,65 +226,190 @@
}
// 加载详情
-async function loadDetail () {
+async function loadDetail() {
if (!formData.value.id) return
const res = await fwDefenseSceneDetailApi({ id: formData.value.id })
formData.value = res?.data?.data ?? initForm()
}
// 获取区域列表
-async function getAreaList () {
+async function getAreaList() {
if (areaList.value.length) return
const res = await fwAreaDivideListApi({ filterSelected: 1, sceneId: formData.value.id })
areaList.value = res?.data?.data ?? []
}
// 关联区域变更
-async function handleAreaSelectionChange (rows) {
- const selectedRows = await ensureAreaGeom(rows)
+async function handleAreaSelectionChange(rows) {
+ const selectedRows = await ensureAreaExtList(rows)
selectedAreaRows.value = selectedRows
const ids = selectedRows.map(item => item.id)
formData.value.areaDivideIds = ids.join(',')
formData.value.areaCount = ids.length
- formData.value.defenseArea = calcDefenseArea(selectedRows)
}
// 渲染面
-function renderingSurface () {
- console.log(geometricSource, 11111111)
-
- if (!geometricSource) {
- initMap()
+function ensureAreaDisplaySource() {
+ if (!viewer) return null
+ if (!areaDisplaySource) {
+ areaDisplaySource = new Cesium.CustomDataSource('scene-area-display')
+ viewer.dataSources.add(areaDisplaySource)
}
- if (!geometricSource) return
- geometricSource && geometricSource.entities.removeAll()
- selectedAreaRows.value.forEach(item => {
- if (item.geom) {
- const pointList = geomAnalysis(item.geom)
- const result = pointList.map(item => [item.longitude, item.latitude]).flat()
- geometricSource.entities?.add({
- customType: 'control_group',
- position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
+ return areaDisplaySource
+}
+
+function clearAreaDisplay() {
+ if (!areaDisplaySource) return
+ areaDisplaySource.entities.removeAll()
+}
+
+function normalizeShapePoint(point) {
+ if (!point) return null
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ return { lng: Number(lng), lat: Number(lat), height }
+}
+
+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 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) {
+ return null
+ }
+}
+
+function resolveAreaShapes(area) {
+ const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
+ if (!extList.length) return []
+ return extList
+ .map((item, index) => {
+ const isShapePayload = item?.drawType || item?.points
+ if (isShapePayload) {
+ return {
+ id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: item?.drawType ?? 'polygon',
+ areaType: item?.areaType ?? item?.areaTypeKey ?? '',
+ points: Array.isArray(item?.points) ? item.points : [],
+ displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
+ meta: item?.meta ?? null,
+ }
+ }
+ const parsed = parseGeomJson(item?.geomJson)
+ if (!parsed) return null
+ return {
+ id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: parsed?.drawType ?? 'polygon',
+ areaType: item?.areaTypeKey ?? parsed?.areaType ?? '',
+ points: Array.isArray(parsed?.points) ? parsed.points : [],
+ displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
+ meta: parsed?.meta ?? null,
+ }
+ })
+ .filter(Boolean)
+}
+
+function renderSelectedAreas() {
+ if (!visible.value) {
+ clearAreaDisplay()
+ return
+ }
+ if (!viewer) initMap()
+ const dataSource = ensureAreaDisplaySource()
+ if (!dataSource) return
+ dataSource.entities.removeAll()
+ selectedAreaRows.value.forEach(area => {
+ const shapes = resolveAreaShapes(area)
+ shapes.forEach(shape => {
+ 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 radii = shape.meta.bufferRadii
+ .map(radius => Number(radius))
+ .filter(radius => Number.isFinite(radius) && radius > 0)
+ radii.forEach((radius, index) => {
+ const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
+ dataSource.entities.add({
+ name: 'scene-area-display',
+ customData: { 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: 'scene-area-display',
+ customData: { drawType: shape.drawType, level: index + 1, outline: true },
+ polyline: {
+ positions: positions.length ? [...positions, positions[0]] : positions,
+ clampToGround: true,
+ width: 2,
+ material: style.outline,
+ },
+ })
+ })
+ return
+ }
+ const positions = buildShapePositions(getShapeDisplayPoints(shape))
+ if (positions.length < 3) return
+ const style = getAreaTypeStyle(shape.areaType)
+ dataSource.entities.add({
+ name: 'scene-area-display',
+ customData: { drawType: shape.drawType },
+ polygon: {
+ hierarchy: new Cesium.PolygonHierarchy(positions),
+ material: style.fill,
+ outline: false,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
polyline: {
- positions: Cesium.Cartesian3.fromDegreesArray(result),
+ positions: positions.length ? [...positions, positions[0]] : positions,
clampToGround: true,
- width: 3,
- material: Cesium.Color.RED,
+ width: 2,
+ material: style.outline,
},
})
- }
+ })
})
}
-watch(() => selectedAreaRows.value, renderingSurface)
+watch(() => selectedAreaRows.value, renderSelectedAreas)
+watch(
+ () => visible.value,
+ val => {
+ if (!val) clearAreaDisplay()
+ }
+)
-function calcDefenseArea (rows) {
- const total = rows.reduce((sum, item) => sum + (Number(item.areaSize) || 0), 0)
- return _.round(total, 2)
-}
-
-async function ensureAreaGeom (rows) {
- const missingRows = rows.filter(row => row?.id && !row.geom)
+async function ensureAreaExtList(rows) {
+ const missingRows = rows.filter(row => row?.id && !Array.isArray(row?.fwAreaDivideExtList))
if (!missingRows.length) return rows
const detailList = await Promise.all(
missingRows.map(row =>
@@ -294,9 +418,7 @@
.catch(() => null)
)
)
- const detailMap = new Map(
- detailList.filter(Boolean).map(item => [String(item.id), item])
- )
+ const detailMap = new Map(detailList.filter(Boolean).map(item => [String(item.id), item]))
if (!detailMap.size) return rows
areaList.value = areaList.value.map(item => {
const detail = detailMap.get(String(item.id))
@@ -309,7 +431,7 @@
}
// 同步关联区域
-function syncAreaSelection () {
+async function syncAreaSelection() {
if (!areaTableRef.value) return
areaTableRef.value.clearSelection()
const rows = []
@@ -317,99 +439,32 @@
const arr = Array.isArray(areaDivideIds)
? areaDivideIds.map(item => String(item))
: String(areaDivideIds || '')
- .split(',')
- .filter(Boolean)
+ .split(',')
+ .filter(Boolean)
areaList.value.forEach(row => {
if (arr.includes(String(row.id))) {
areaTableRef.value.toggleRowSelection(row, true)
rows.push(row)
}
})
- selectedAreaRows.value = rows
- if (!rows.length) return
- formData.value.areaCount = rows.length
- formData.value.defenseArea = calcDefenseArea(rows)
+ const ensuredRows = await ensureAreaExtList(rows)
+ selectedAreaRows.value = ensuredRows
+ if (!ensuredRows.length) return
+ formData.value.areaCount = ensuredRows.length
}
-function formatDefenseArea (value) {
- if (value == null || value === '') return ''
- return `${value}k㎡`
-}
-
-// 格式化指挥点位置
-function formatLocation (row) {
- if (row?.longitude == null || row?.latitude == null) return ''
- return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
-}
-
-let geometricSource
// 初始化地图实例
-function initMap () {
+function initMap() {
if (viewer) return
const map = mapRef.value?.getMap()
if (!map?.viewer) return
viewer = map.viewer
- if (!readonly.value && !leftClickBound) {
- map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
- leftClickBound = true
- }
- if (!geometricSource) {
- geometricSource = new Cesium.CustomDataSource('geometricSource')
- viewer.dataSources.add(geometricSource)
- }
}
-function LeftClickEvent (click) {
- const pos = click.position
- const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
- if (!cartesian3) return
- const carto = Cesium.Cartographic.fromCartesian(cartesian3)
- const longitude = Cesium.Math.toDegrees(carto.longitude)
- const latitude = Cesium.Math.toDegrees(carto.latitude)
- formData.value.longitude = _.round(longitude, 6)
- formData.value.latitude = _.round(latitude, 6)
- setMapPoint(formData.value.longitude, formData.value.latitude)
-}
-
-function setMapPoint (longitude, latitude) {
- if (!viewer || longitude == null || latitude == null) return
- if (!redPointEntity) {
- redPointEntity = viewer.entities.add({
- position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
- billboard: {
- image: commandPost, // 这里替换为你的图片路径或变量 deviceZcImg
- width: 40,
- height: 56,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 设置图片底部对齐坐标点
- },
- label: {
- text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
- font: '14px',
- fillColor: Cesium.Color.WHITE,
- backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
- backgroundPadding: new Cesium.Cartesian2(5, 5),
- showBackground: true,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- outlineWidth: 1,
- outlineColor: Cesium.Color.WHITE,
- pixelOffset: new Cesium.Cartesian2(0, -72),
- eyeOffset: new Cesium.Cartesian3(0, 0, -20),
- },
- })
- } else {
- redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
- redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
- }
-}
-
-// 打开弹框
-async function open ({ mode, row } = {}) {
+async function open({ mode, row } = {}) {
dialogMode.value = mode || 'add'
formData.value = dialogMode.value === 'add' ? initForm() : row
selectedAreaRows.value = []
- redPointEntity = null
await nextTick()
initMap()
await getAreaList()
@@ -417,14 +472,8 @@
await loadDetail()
}
await nextTick()
- syncAreaSelection()
- setMapPoint(formData.value.longitude, formData.value.latitude)
+ await syncAreaSelection()
}
-
-onMounted(() => {
- getAreaList()
-})
-
defineExpose({
open,
})
diff --git a/applications/drone-command/src/views/areaManage/sceneConfig/index.vue b/applications/drone-command/src/views/areaManage/sceneConfig/index.vue
index 817b481..75de78d 100644
--- a/applications/drone-command/src/views/areaManage/sceneConfig/index.vue
+++ b/applications/drone-command/src/views/areaManage/sceneConfig/index.vue
@@ -1,7 +1,7 @@
<template>
<basic-container>
<el-form ref="queryParamsRef" :model="searchParams" class="command-page-history-search">
- <el-form-item label="场景名称" prop="sceneName">
+ <el-form-item label="场景配置名称" prop="sceneName">
<el-input
class="command-input"
v-model="searchParams.sceneName"
@@ -46,19 +46,12 @@
<el-table-column type="selection" width="46" />
<el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
- <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
+ <el-table-column prop="sceneName" show-overflow-tooltip label="场景配置名称" />
<el-table-column prop="sceneType" show-overflow-tooltip label="场景类型">
<template v-slot="{ row }">
{{ getDictLabel(row.sceneType, dictObj.CommandSceneType) }}
</template>
</el-table-column>
- <el-table-column show-overflow-tooltip label="指挥点位置">
- <template v-slot="{ row }">
- {{ formatLocation(row) }}
- </template>
- </el-table-column>
- <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
- <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
<el-table-column prop="areaCount" show-overflow-tooltip label="区域数量" />
<el-table-column prop="counterDeviceCount" show-overflow-tooltip label="反制设备数量" />
<el-table-column prop="detectDeviceCount" show-overflow-tooltip label="探测设备数量" />
@@ -100,7 +93,7 @@
import { useRoute } from 'vue-router'
const initSearchParams = () => ({
- sceneName: '', // 场景名称
+ sceneName: '', // 场景配置名称
sceneType: '', // 场景类型
current: 1, // 当前页
size: 10, // 每页大小
diff --git a/applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue
new file mode 100644
index 0000000..e3818e6
--- /dev/null
+++ b/applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue
@@ -0,0 +1,553 @@
+<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>
+ <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.sceneName }}</div>
+ </el-col>
+ <el-col :span="24">
+ <div class="label">负责人</div>
+ <div class="val">{{ formData.defenseLeader }}</div>
+ </el-col>
+ <el-col :span="24">
+ <div class="label">负责人电话</div>
+ <div class="val">{{ formData.leaderPhone }}</div>
+ </el-col>
+ <el-col :span="24">
+ <div class="label">有效时间</div>
+ <div class="val">{{ formatEffectiveDate(formData) }}</div>
+ </el-col>
+ <el-col :span="24">
+ <div class="label">指挥点位置</div>
+ <div class="val">{{ formatLocation(formData) }}</div>
+ </el-col>
+ <el-col :span="24">
+ <div class="label">关联场景配置</div>
+ <div class="val">{{ getSceneConfigName(formData.defenseSceneId) }}</div>
+ </el-col>
+ </el-row>
+ </div>
+
+ <el-form
+ class="dialog-form"
+ v-else
+ ref="formRef"
+ :model="formData"
+ :rules="rules"
+ :disabled="readonly"
+ label-width="110px"
+ >
+ <el-form-item label="场景名称" prop="sceneName">
+ <el-input
+ class="command-input"
+ v-model="formData.sceneName"
+ maxlength="50"
+ placeholder="请输入"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="关联场景配置" prop="defenseSceneId">
+ <el-select
+ class="command-select"
+ popper-class="command-select-popper"
+ v-model="formData.defenseSceneId"
+ placeholder="请选择"
+ clearable
+ >
+ <el-option v-for="item in sceneConfigList" :key="item.id" :label="item.sceneName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="负责人" prop="defenseLeader">
+ <el-input
+ class="command-input"
+ v-model="formData.defenseLeader"
+ maxlength="50"
+ placeholder="请输入"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="负责人电话" prop="leaderPhone">
+ <el-input
+ class="command-input"
+ v-model="formData.leaderPhone"
+ maxlength="50"
+ placeholder="请输入"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="有效时间范围" prop="effectiveDateStart">
+ <el-date-picker
+ class="command-date-picker"
+ popper-class="command-date-picker-popper"
+ v-model="effectiveDateRange"
+ type="datetimerange"
+ range-separator="至"
+ start-placeholder="开始时间"
+ end-placeholder="结束时间"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="指挥点位置" prop="longitude">
+ <div class="val">{{ formatLocation(formData) }}</div>
+ </el-form-item>
+ </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, nextTick, onMounted, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { fwDefenseSceneManageDetailApi, fwDefenseSceneManageSubmitApi } from './sceneManageApi'
+import { fwDefenseSceneListApi } from '../sceneConfig/sceneConfigApi'
+import { fieldRules } from '@ztzf/utils'
+import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
+import * as Cesium from 'cesium'
+import commandPost from '@/assets/images/dataCockpit/legend/command-post.png'
+import { saveOperationLog } from '@ztzf/apis'
+import { useRoute } from 'vue-router'
+import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
+import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
+
+// 初始化表单数据
+const initForm = () => ({
+ sceneName: '', // 场景名称
+ defenseLeader: '', // 防控负责人
+ leaderPhone: '', // 防控负责人电话
+ effectiveDateStart: '', // 有效时间-开始
+ effectiveDateEnd: '', // 有效时间-结束
+ longitude: null, // 指挥点经度
+ latitude: null, // 指挥点纬度
+ defenseSceneId: '', // 关联场景配置ID
+})
+
+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 titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
+const effectiveDateRange = ref([]) // 有效时间范围
+const sceneConfigList = ref([]) // 场景配置列表
+const areaList = ref([]) // ????
+const mapRef = ref(null)
+const route = useRoute()
+let viewer
+let redPointEntity
+let areaDisplaySource
+let leftClickBound = false
+
+// 表单验证规则
+const rules = {
+ sceneName: fieldRules(true, 50),
+ defenseLeader: fieldRules(true, 50),
+ leaderPhone: fieldRules(true, 50),
+ effectiveDateStart: fieldRules(true),
+ longitude: fieldRules(true),
+ defenseSceneId: fieldRules(true),
+}
+
+// 关闭弹框
+function handleCancel() {
+ visible.value = false
+}
+
+// 提交新增/编辑
+async function handleSubmit() {
+ const isValid = await formRef.value?.validate().catch(() => false)
+ if (!isValid) return
+ submitting.value = true
+ try {
+ await fwDefenseSceneManageSubmitApi(formData.value)
+ saveOperationLog({
+ requestUri: route.path,
+ title: `${route.name || '场景管理'}-${dialogMode.value === 'add' ? '新增' : '编辑'}`,
+ type: 1,
+ })
+ ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
+ visible.value = false
+ emit('success')
+ } finally {
+ submitting.value = false
+ }
+}
+
+// 监听有效时间范围变化,同步到表单数据
+watch(
+ () => effectiveDateRange.value,
+ () => {
+ formData.value.effectiveDateStart = effectiveDateRange.value?.[0] || ''
+ formData.value.effectiveDateEnd = effectiveDateRange.value?.[1] || ''
+ }
+)
+
+// 加载详情
+async function loadDetail() {
+ if (!formData.value.id) return
+ const res = await fwDefenseSceneManageDetailApi({ id: formData.value.id })
+ formData.value = res?.data?.data ?? initForm()
+ // 同步有效时间范围
+ effectiveDateRange.value = [formData.value.effectiveDateStart, formData.value.effectiveDateEnd].filter(Boolean)
+}
+
+// 获取场景配置列表
+// ?????
+function ensureAreaDisplaySource() {
+ if (!viewer) return null
+ if (!areaDisplaySource) {
+ areaDisplaySource = new Cesium.CustomDataSource('scene-manage-area-display')
+ viewer.dataSources.add(areaDisplaySource)
+ }
+ return areaDisplaySource
+}
+
+function clearAreaDisplay() {
+ if (!areaDisplaySource) return
+ areaDisplaySource.entities.removeAll()
+}
+
+function normalizeShapePoint(point) {
+ if (!point) return null
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ return { lng: Number(lng), lat: Number(lat), height }
+}
+
+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 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) {
+ return null
+ }
+}
+
+function resolveAreaShapes(area) {
+ const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
+ if (!extList.length) return []
+ return extList
+ .map((item, index) => {
+ const isShapePayload = item?.drawType || item?.points
+ if (isShapePayload) {
+ return {
+ id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: item?.drawType ?? 'polygon',
+ areaType: item?.areaType ?? item?.areaTypeKey ?? '',
+ points: Array.isArray(item?.points) ? item.points : [],
+ displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
+ meta: item?.meta ?? null,
+ }
+ }
+ const parsed = parseGeomJson(item?.geomJson)
+ if (!parsed) return null
+ return {
+ id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: parsed?.drawType ?? 'polygon',
+ areaType: item?.areaTypeKey ?? parsed?.areaType ?? '',
+ points: Array.isArray(parsed?.points) ? parsed.points : [],
+ displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
+ meta: parsed?.meta ?? null,
+ }
+ })
+ .filter(Boolean)
+}
+
+function renderSceneAreas() {
+ if (!visible.value) {
+ clearAreaDisplay()
+ return
+ }
+ if (!viewer) initMap()
+ const dataSource = ensureAreaDisplaySource()
+ if (!dataSource) return
+ dataSource.entities.removeAll()
+ areaList.value.forEach(area => {
+ const shapes = resolveAreaShapes(area)
+ shapes.forEach(shape => {
+ 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 radii = shape.meta.bufferRadii
+ .map(radius => Number(radius))
+ .filter(radius => Number.isFinite(radius) && radius > 0)
+ radii.forEach((radius, index) => {
+ const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
+ dataSource.entities.add({
+ name: 'scene-manage-area-display',
+ customData: { 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: 'scene-manage-area-display',
+ customData: { drawType: shape.drawType, level: index + 1, outline: true },
+ polyline: {
+ positions: positions.length ? [...positions, positions[0]] : positions,
+ clampToGround: true,
+ width: 2,
+ material: style.outline,
+ },
+ })
+ })
+ return
+ }
+ const positions = buildShapePositions(getShapeDisplayPoints(shape))
+ if (positions.length < 3) return
+ const style = getAreaTypeStyle(shape.areaType)
+ dataSource.entities.add({
+ name: 'scene-manage-area-display',
+ customData: { 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,
+ },
+ })
+ })
+ })
+}
+
+async function ensureAreaExtList(rows) {
+ const missingRows = rows.filter(row => row?.id && !Array.isArray(row?.fwAreaDivideExtList))
+ if (!missingRows.length) return rows
+ const detailList = await Promise.all(
+ missingRows.map(row =>
+ fwAreaDivideDetailApi({ id: row.id })
+ .then(res => res?.data?.data)
+ .catch(() => null)
+ )
+ )
+ const detailMap = new Map(detailList.filter(Boolean).map(item => [String(item.id), item]))
+ if (!detailMap.size) return rows
+ return rows.map(item => {
+ const detail = detailMap.get(String(item.id))
+ return detail ? { ...item, ...detail } : item
+ })
+}
+
+// ??????????
+async function loadAreaList(sceneId) {
+ if (!sceneId) {
+ areaList.value = []
+ clearAreaDisplay()
+ return
+ }
+ const res = await fwAreaDivideListApi({ sceneId })
+ const list = res?.data?.data ?? []
+ areaList.value = await ensureAreaExtList(list)
+ renderSceneAreas()
+}
+
+async function getSceneConfigList() {
+ if (sceneConfigList.value.length) return
+ const res = await fwDefenseSceneListApi({unbound: 1})
+ sceneConfigList.value = res?.data?.data ?? []
+}
+
+// 获取场景配置名称
+function getSceneConfigName(id) {
+ if (!id) return ''
+ const item = sceneConfigList.value.find(item => item.id === id)
+ return item?.sceneName || id
+}
+
+// 格式化指挥点位置
+function formatLocation(row) {
+ if (row?.longitude == null || row?.latitude == null) return ''
+ return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
+}
+
+// 格式化有效时间
+function formatEffectiveDate(row) {
+ if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
+ return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
+}
+
+// 初始化地图实例
+function initMap() {
+ if (viewer) return
+ const map = mapRef.value?.getMap()
+ if (!map?.viewer) return
+ viewer = map.viewer
+ if (!readonly.value && !leftClickBound) {
+ map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
+ leftClickBound = true
+ }
+}
+
+// 地图左键点击事件
+async function LeftClickEvent(click) {
+ const pos = click.position
+ // 优先使用 pickPosition 获取贴地坐标(包含地形高度)
+ let cartesian3 = viewer.scene.pickPosition(pos)
+ // 如果 pickPosition 失败,回退到 pickEllipsoid
+ if (!cartesian3 || !Cesium.defined(cartesian3)) {
+ cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
+ }
+ if (!cartesian3) return
+ const carto = Cesium.Cartographic.fromCartesian(cartesian3)
+ const longitude = Cesium.Math.toDegrees(carto.longitude)
+ const latitude = Cesium.Math.toDegrees(carto.latitude)
+ formData.value.longitude = _.round(longitude, 6)
+ formData.value.latitude = _.round(latitude, 6)
+ setMapPoint(formData.value.longitude, formData.value.latitude)
+ await loadAreaList(formData.value.defenseSceneId)
+}
+
+// 设置地图点位
+function setMapPoint(longitude, latitude) {
+ if (!viewer || longitude == null || latitude == null) return
+ if (!redPointEntity) {
+ redPointEntity = viewer.entities.add({
+ position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
+ billboard: {
+ image: commandPost,
+ width: 40,
+ height: 56,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ label: {
+ text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
+ font: '14px',
+ fillColor: Cesium.Color.WHITE,
+ backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
+ backgroundPadding: new Cesium.Cartesian2(5, 5),
+ showBackground: true,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
+ outlineWidth: 1,
+ outlineColor: Cesium.Color.WHITE,
+ pixelOffset: new Cesium.Cartesian2(0, -72),
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ })
+ } else {
+ redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
+ redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
+ }
+}
+
+// 打开弹框
+async function open({ mode, row } = {}) {
+ dialogMode.value = mode || 'add'
+ formData.value = dialogMode.value === 'add' ? initForm() : { ...row }
+ effectiveDateRange.value = []
+ redPointEntity = null
+ await nextTick()
+ initMap()
+ await getSceneConfigList()
+ if (dialogMode.value !== 'add') {
+ await loadDetail()
+ }
+ await nextTick()
+ setMapPoint(formData.value.longitude, formData.value.latitude)
+ await loadAreaList(formData.value.defenseSceneId)
+}
+
+onMounted(() => {
+ getSceneConfigList()
+})
+
+watch(
+ () => formData.value.defenseSceneId,
+ sceneId => {
+ if (!visible.value) return
+ loadAreaList(sceneId)
+ }
+)
+
+watch(
+ () => visible.value,
+ val => {
+ if (!val) clearAreaDisplay()
+ }
+)
+
+
+defineExpose({
+ open,
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/areaManage/sceneManage/index.vue b/applications/drone-command/src/views/areaManage/sceneManage/index.vue
new file mode 100644
index 0000000..f9f4689
--- /dev/null
+++ b/applications/drone-command/src/views/areaManage/sceneManage/index.vue
@@ -0,0 +1,172 @@
+<template>
+ <basic-container>
+ <el-form ref="queryParamsRef" :model="searchParams" class="command-page-history-search">
+ <el-form-item label="场景名称" prop="sceneName">
+ <el-input
+ class="command-input"
+ v-model="searchParams.sceneName"
+ placeholder="请输入"
+ clearable
+ @clear="handleSearch"
+ />
+ </el-form-item>
+
+ <el-form-item class="history-search-actions">
+ <el-button :icon="RefreshRight" @click="resetForm"></el-button>
+ <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button>
+ </el-form-item>
+ </el-form>
+
+ <div class="command-table-toolbar">
+ <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
+ <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
+ </div>
+
+ <div class="command-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
+ <div class="command-table-content command-table-content-bg">
+ <el-table class="command-table" :data="list" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="46" />
+ <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
+
+ <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
+ <el-table-column show-overflow-tooltip label="指挥点经度纬度">
+ <template v-slot="{ row }">
+ {{ formatLocation(row) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
+ <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
+ <el-table-column show-overflow-tooltip label="有效时间" width="300">
+ <template v-slot="{ row }">
+ {{ formatEffectiveDate(row) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="defenseSceneName" show-overflow-tooltip label="关联场景配置" />
+ <el-table-column label="操作" class-name="operation-btns">
+ <template v-slot="{ row }">
+ <el-link @click="openForm('view', row)">查看</el-link>
+ <el-link @click="openForm('edit', row)">编辑</el-link>
+ <el-link @click="handleDelete(row)">删除</el-link>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div class="command-table-pagination">
+ <el-pagination
+ popper-class="command-select-popper"
+ v-model:current-page="searchParams.current"
+ v-model:page-size="searchParams.size"
+ layout="total, prev, pager, next, sizes"
+ :total="total"
+ @change="getList"
+ />
+ </div>
+ </div>
+
+ <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" />
+ </basic-container>
+</template>
+
+<script setup>
+import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
+import { nextTick, onMounted, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { fwDefenseSceneManagePageApi, fwDefenseSceneManageRemoveApi } from './sceneManageApi'
+import FormDiaLog from './FormDiaLog.vue'
+import { saveOperationLog } from '@ztzf/apis'
+import { useRoute } from 'vue-router'
+
+// 初始化查询参数
+const initSearchParams = () => ({
+ sceneName: '', // 场景名称
+ current: 1, // 当前页
+ size: 10, // 每页大小
+})
+
+const searchParams = ref(initSearchParams()) // 查询参数
+const total = ref(0) // 总条数
+const loading = ref(true) // 列表加载中
+const list = ref([]) // 列表数据
+const selectedIds = ref([]) // 勾选的ID列表
+const queryParamsRef = ref(null) // 查询表单实例
+const dialogRef = ref(null) // 弹框实例
+const dialogVisible = ref(false)
+const route = useRoute()
+
+// 获取列表数据
+async function getList() {
+ loading.value = true
+ try {
+ const res = await fwDefenseSceneManagePageApi(searchParams.value)
+ list.value = res?.data?.data?.records ?? []
+ total.value = res?.data?.data?.total ?? 0
+ } finally {
+ loading.value = false
+ }
+}
+
+// 查询
+function handleSearch() {
+ searchParams.value.current = 1
+ getList()
+}
+
+// 重置查询
+function resetForm() {
+ queryParamsRef.value?.resetFields()
+ searchParams.value.current = 1
+ getList()
+}
+
+// 删除
+async function handleDelete(row) {
+ const tips = row ? '该条' : '选中的项'
+ await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', {
+ type: 'warning',
+ customClass: 'command-page-view-message-box',
+ confirmButtonClass: 'command-message-box-confirm',
+ cancelButtonClass: 'command-message-box-cancel',
+ })
+ const ids = row ? row.id : selectedIds.value.join(',')
+ await fwDefenseSceneManageRemoveApi({ ids })
+ saveOperationLog({
+ requestUri: route.path,
+ title: `${route.name || '场景管理'}-删除`,
+ type: 1,
+ })
+ ElMessage.success('删除成功')
+ selectedIds.value = []
+ getList()
+}
+
+// 勾选值设置
+function handleSelectionChange(rows) {
+ selectedIds.value = rows.map(item => item.id)
+}
+
+// 格式化指挥点位置
+function formatLocation(row) {
+ if (row?.longitude == null || row?.latitude == null) return ''
+ return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
+}
+
+// 格式化有效时间
+function formatEffectiveDate(row) {
+ if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
+ return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
+}
+
+// 新增/编辑/查看 弹框
+function openForm(mode, row) {
+ dialogVisible.value = true
+ nextTick(() => {
+ dialogRef.value?.open({ mode, row })
+ })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js b/applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js
new file mode 100644
index 0000000..77464c3
--- /dev/null
+++ b/applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js
@@ -0,0 +1,38 @@
+import request from '@/axios'
+
+// 分页查询场景管理列表
+export const fwDefenseSceneManagePageApi = params => {
+ return request({
+ url: `/drone-fw/area/fwDefenseSceneManage/page`,
+ method: 'get',
+ params: { descs: 'update_time', ...params },
+ })
+}
+
+// 获取场景管理详情
+export const fwDefenseSceneManageDetailApi = params => {
+ return request({
+ url: `/drone-fw/area/fwDefenseSceneManage/detail`,
+ method: 'get',
+ params,
+ })
+}
+
+// 新增或修改场景管理
+export const fwDefenseSceneManageSubmitApi = data => {
+ return request({
+ url: `/drone-fw/area/fwDefenseSceneManage/submit`,
+ method: 'post',
+ data,
+ })
+}
+
+// 删除场景管理
+export const fwDefenseSceneManageRemoveApi = params => {
+ return request({
+ url: `/drone-fw/area/fwDefenseSceneManage/remove`,
+ method: 'post',
+ params,
+ })
+}
+
diff --git a/applications/drone-command/src/views/audioCall.vue b/applications/drone-command/src/views/audioCall.vue
index 55de074..90910ac 100644
--- a/applications/drone-command/src/views/audioCall.vue
+++ b/applications/drone-command/src/views/audioCall.vue
@@ -1,439 +1,439 @@
<template>
<div style="padding:16px">
- <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
-
- <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
- <span>我的 userId:</span>
- <input v-model="uid" style="width:120px" />
-
- <span>对方 userId:</span>
- <input v-model="peerUid" style="width:120px" />
-
- <button @click="connectWS" :disabled="connected">连接WS</button>
-
- <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
-
- <button v-if="state==='ringing'" @click="acceptCall">接听</button>
- <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
-
- <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
-
- <strong>状态:{{ state }}</strong>
- <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
- </div>
-
- <div style="margin-top:8px;color:#666">
- <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
- </div>
-
- <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
-
- <audio ref="remoteAudio" autoplay></audio>
+ <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
+
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
+ <span>我的 userId:</span>
+ <input v-model="uid" style="width:120px" />
+
+ <span>对方 userId:</span>
+ <input v-model="peerUid" style="width:120px" />
+
+ <button @click="connectWS" :disabled="connected">连接WS</button>
+
+ <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
+
+ <button v-if="state==='ringing'" @click="acceptCall">接听</button>
+ <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
+
+ <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
+
+ <strong>状态:{{ state }}</strong>
+ <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
+ </div>
+
+ <div style="margin-top:8px;color:#666">
+ <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
+ </div>
+
+ <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
+
+ <audio ref="remoteAudio" autoplay></audio>
</div>
- </template>
-
- <script setup>
- import { ref, computed, onBeforeUnmount } from 'vue'
-
- /** ✅ 你的 WS 地址前缀(后面拼 userId) */
- const WS_BASE = 'wss://域名/ws/chat?userId='
-
- const uid = ref('2')
- const peerUid = ref('3')
-
- const connected = ref(false)
- const state = ref('idle') // idle | calling | ringing | in-call
- const logText = ref('')
- const remoteAudio = ref(null)
-
- let ws = null
- let pc = null
- let localStream = null
- const pendingCandidates = []
-
- // 来电暂存
- let incomingFrom = null
- let acceptedByMe = false // 我是否已经点了接听
- let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
-
- // 计时
- let timer = null
- const seconds = ref(0)
- const durationText = computed(() => {
+</template>
+
+<script setup>
+import { ref, computed, onBeforeUnmount } from 'vue'
+
+/** ✅ 你的 WS 地址前缀(后面拼 userId) */
+const WS_BASE = 'wss://域名/ws/chat?userId='
+
+const uid = ref('2')
+const peerUid = ref('3')
+
+const connected = ref(false)
+const state = ref('idle') // idle | calling | ringing | in-call
+const logText = ref('')
+const remoteAudio = ref(null)
+
+let ws = null
+let pc = null
+let localStream = null
+const pendingCandidates = []
+
+// 来电暂存
+let incomingFrom = null
+let acceptedByMe = false // 我是否已经点了接听
+let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
+
+// 计时
+let timer = null
+const seconds = ref(0)
+const durationText = computed(() => {
const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
const s = String(seconds.value % 60).padStart(2, '0')
return `${m}:${s}`
- })
- function startTimer() {
+})
+function startTimer() {
stopTimer()
seconds.value = 0
timer = setInterval(() => seconds.value++, 1000)
- }
- function stopTimer() {
+}
+function stopTimer() {
if (timer) clearInterval(timer)
timer = null
- }
-
- // 心跳
- let pingTimer = null
- function startPing() {
+}
+
+// 心跳
+let pingTimer = null
+function startPing() {
stopPing()
// 每 25 秒发一次 ping(你后端有 PING/PONG)
pingTimer = setInterval(() => {
- if (connected.value) send('ping', 'system', null)
+ if (connected.value) send('ping', 'system', null)
}, 25000)
- }
- function stopPing() {
+}
+function stopPing() {
if (pingTimer) clearInterval(pingTimer)
pingTimer = null
- }
-
- function log(...args) {
+}
+
+function log(...args) {
const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
console.log(...args)
- }
-
- /**
- * ✅ 严格按后端 ChatMessage 字段发:
- * { type, from, to, payload, timestamp }
- * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
- */
- function send(type, to, payload) {
+}
+
+/**
+ * ✅ 严格按后端 ChatMessage 字段发:
+ * { type, from, to, payload, timestamp }
+ * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
+ */
+function send(type, to, payload) {
if (!type) return
if (!ws || ws.readyState !== WebSocket.OPEN) return
-
+
const msg = {
- type, // "call/accept/busy/offer/answer/candidate/hangup/ping"
- from: String(uid.value),
- to: String(to),
- payload: payload ?? null,
- timestamp: Date.now(),
+ type, // "call/accept/busy/offer/answer/candidate/hangup/ping"
+ from: String(uid.value),
+ to: String(to),
+ payload: payload ?? null,
+ timestamp: Date.now(),
}
ws.send(JSON.stringify(msg))
- }
-
- /* ---------------- WebRTC ---------------- */
-
- function createPC() {
+}
+
+/* ---------------- WebRTC ---------------- */
+
+function createPC() {
pc = new RTCPeerConnection({
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
})
-
+
pc.onicecandidate = (e) => {
- if (e.candidate) {
- send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
- }
+ if (e.candidate) {
+ send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
+ }
}
-
+
pc.ontrack = (e) => {
- log('✅ 收到远端音频流')
- const stream = e.streams[0]
- if (remoteAudio.value) {
- remoteAudio.value.srcObject = stream
- remoteAudio.value.play().catch(() => {})
- }
+ log('✅ 收到远端音频流')
+ const stream = e.streams[0]
+ if (remoteAudio.value) {
+ remoteAudio.value.srcObject = stream
+ remoteAudio.value.play().catch(() => {})
+ }
}
-
+
pc.onconnectionstatechange = () => {
- log('pc.connectionState =', pc.connectionState)
+ log('pc.connectionState =', pc.connectionState)
}
- }
-
- async function getLocalAudio() {
+}
+
+async function getLocalAudio() {
if (localStream) return localStream
-
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
- throw new Error('getUserMedia not available')
+ log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
+ throw new Error('getUserMedia not available')
}
-
+
try {
- localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
- log('✅ 已获取本地麦克风')
- return localStream
+ localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
+ log('✅ 已获取本地麦克风')
+ return localStream
} catch (e) {
- log('❌ 获取麦克风失败:', String(e))
- throw e
+ log('❌ 获取麦克风失败:', String(e))
+ throw e
}
- }
-
- /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
- async function ensureLocalTracksAdded() {
+}
+
+/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
+async function ensureLocalTracksAdded() {
if (!pc) createPC()
const stream = await getLocalAudio()
-
+
const existingTrackIds = new Set(
- pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
+ pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
)
-
+
stream.getTracks().forEach(track => {
- if (!existingTrackIds.has(track.id)) {
- pc.addTrack(track, stream)
- log('addTrack ok:', track.id)
- } else {
- // log('skip addTrack:', track.id)
- }
+ if (!existingTrackIds.has(track.id)) {
+ pc.addTrack(track, stream)
+ log('addTrack ok:', track.id)
+ } else {
+ // log('skip addTrack:', track.id)
+ }
})
-
+
return stream
- }
-
- async function flushCandidates() {
+}
+
+async function flushCandidates() {
if (!pc || !pc.remoteDescription) return
while (pendingCandidates.length) {
- const c = pendingCandidates.shift()
- try {
- await pc.addIceCandidate(new RTCIceCandidate(c))
- } catch (e) {
- log('addIceCandidate failed:', String(e))
- }
+ const c = pendingCandidates.shift()
+ try {
+ await pc.addIceCandidate(new RTCIceCandidate(c))
+ } catch (e) {
+ log('addIceCandidate failed:', String(e))
+ }
}
- }
-
- /* ---------------- 信令处理 ---------------- */
-
- async function onSignal(msg) {
+}
+
+/* ---------------- 信令处理 ---------------- */
+
+async function onSignal(msg) {
if (!msg) return
const t = (msg.type || '').toString()
-
+
// ✅ 忽略后端回显给自己的消息(你的后端会回显 sender)
if (msg.from && String(msg.from) === String(uid.value)) {
- return
+ return
}
-
+
// system 消息:login/pong/busy 仅记录
if (msg.from === 'system') {
- log('system msg:', msg)
- return
- }
-
- if (!t) return
-
- if (t === 'call') {
- // 收到来电:弹接听按钮
- incomingFrom = String(msg.from)
- peerUid.value = incomingFrom
- acceptedByMe = false
- offeredByPeer = false
-
- state.value = 'ringing'
- log('📞 收到来电 call,来自', incomingFrom)
- return
- }
-
- if (t === 'accept') {
- // 主叫收到 accept:开始 offer
- log('✅ 对方接听 accept,开始建立连接(offer)')
- await startOffer()
- return
- }
-
- if (t === 'busy') {
- log('❌ 对方忙线/拒绝')
- cleanup(false)
- return
- }
-
- if (t === 'offer') {
- // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
- offeredByPeer = true
- incomingFrom = String(msg.from)
- peerUid.value = incomingFrom
-
- if (!acceptedByMe) {
- state.value = 'ringing'
- log('📩 收到 offer(但未接听),已等待用户点击接听')
- // 暂存 offer 到一个变量
- lastOfferSdp = msg.payload?.sdp
+ log('system msg:', msg)
return
- }
-
- // 已接听:直接 answer
- await answerOffer(msg.payload?.sdp, incomingFrom)
- return
}
-
+
+ if (!t) return
+
+ if (t === 'call') {
+ // 收到来电:弹接听按钮
+ incomingFrom = String(msg.from)
+ peerUid.value = incomingFrom
+ acceptedByMe = false
+ offeredByPeer = false
+
+ state.value = 'ringing'
+ log('📞 收到来电 call,来自', incomingFrom)
+ return
+ }
+
+ if (t === 'accept') {
+ // 主叫收到 accept:开始 offer
+ log('✅ 对方接听 accept,开始建立连接(offer)')
+ await startOffer()
+ return
+ }
+
+ if (t === 'busy') {
+ log('❌ 对方忙线/拒绝')
+ cleanup(false)
+ return
+ }
+
+ if (t === 'offer') {
+ // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
+ offeredByPeer = true
+ incomingFrom = String(msg.from)
+ peerUid.value = incomingFrom
+
+ if (!acceptedByMe) {
+ state.value = 'ringing'
+ log('📩 收到 offer(但未接听),已等待用户点击接听')
+ // 暂存 offer 到一个变量
+ lastOfferSdp = msg.payload?.sdp
+ return
+ }
+
+ // 已接听:直接 answer
+ await answerOffer(msg.payload?.sdp, incomingFrom)
+ return
+ }
+
if (t === 'answer') {
- log('✅ 收到 answer,通话建立')
- if (!pc) createPC()
- await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
- await flushCandidates()
-
- state.value = 'in-call'
- startTimer()
- return
+ log('✅ 收到 answer,通话建立')
+ if (!pc) createPC()
+ await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
+ await flushCandidates()
+
+ state.value = 'in-call'
+ startTimer()
+ return
}
-
+
if (t === 'candidate') {
- const c = msg.payload?.candidate
- if (!c) return
- if (pc && pc.remoteDescription) {
- await pc.addIceCandidate(new RTCIceCandidate(c))
- } else {
- pendingCandidates.push(c)
- }
- return
+ const c = msg.payload?.candidate
+ if (!c) return
+ if (pc && pc.remoteDescription) {
+ await pc.addIceCandidate(new RTCIceCandidate(c))
+ } else {
+ pendingCandidates.push(c)
+ }
+ return
}
-
+
if (t === 'hangup') {
- log('📴 对方挂断')
- cleanup(false)
- return
+ log('📴 对方挂断')
+ cleanup(false)
+ return
}
-
+
if (t === 'pong') {
- // 心跳响应
- // log('pong')
- return
+ // 心跳响应
+ // log('pong')
+ return
}
- }
-
- // 暂存 offer(用于“先点接听再 answer”)
- let lastOfferSdp = null
-
- /* ---------------- 业务操作 ---------------- */
-
- function connectWS() {
+}
+
+// 暂存 offer(用于“先点接听再 answer”)
+let lastOfferSdp = null
+
+/* ---------------- 业务操作 ---------------- */
+
+function connectWS() {
if (ws) ws.close()
-
+
ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
-
+
ws.onopen = () => {
- connected.value = true
- log('WS 已连接 userId=', uid.value)
- startPing()
+ connected.value = true
+ log('WS 已连接 userId=', uid.value)
+ startPing()
}
-
+
ws.onclose = () => {
- connected.value = false
- log('WS 已关闭')
- stopPing()
+ connected.value = false
+ log('WS 已关闭')
+ stopPing()
}
-
+
ws.onerror = (e) => log('WS error', e)
-
+
ws.onmessage = (e) => {
- const msg = JSON.parse(e.data)
- log('收到信令', msg)
- onSignal(msg)
+ const msg = JSON.parse(e.data)
+ log('收到信令', msg)
+ onSignal(msg)
}
- }
-
- function requestCall() {
+}
+
+function requestCall() {
state.value = 'calling'
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
-
+
send('call', peerUid.value, null)
log('➡️ 发起呼叫 call 给', peerUid.value)
- }
-
- function acceptCall() {
+}
+
+function acceptCall() {
if (!incomingFrom) return
acceptedByMe = true
-
+
// 先告诉对方我接听了(后端会把双方置忙)
send('accept', incomingFrom, null)
log('✅ 已点击接听,发送 accept 给', incomingFrom)
-
+
state.value = 'calling'
-
+
// 如果 offer 已经提前到达(我们暂存了),立刻 answer
if (lastOfferSdp) {
- answerOffer(lastOfferSdp, incomingFrom)
- lastOfferSdp = null
+ answerOffer(lastOfferSdp, incomingFrom)
+ lastOfferSdp = null
}
- }
-
- function rejectCall() {
+}
+
+function rejectCall() {
if (!incomingFrom) return
-
+
// 你后端没有 reject,用 busy 表示拒绝/不可接听
send('busy', incomingFrom, null)
log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
-
+
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
state.value = 'idle'
- }
-
- async function startOffer() {
+}
+
+async function startOffer() {
if (!pc) createPC()
-
+
// ✅ 不会重复 addTrack
await ensureLocalTracksAdded()
-
+
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
-
+
send('offer', peerUid.value, { sdp: pc.localDescription })
log('➡️ 已发送 offer 给', peerUid.value)
- }
-
- async function answerOffer(offerSdp, from) {
+}
+
+async function answerOffer(offerSdp, from) {
if (!offerSdp) {
- log('❌ offer sdp 为空,无法接听')
- return
+ log('❌ offer sdp 为空,无法接听')
+ return
}
if (!pc) createPC()
-
+
await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
await flushCandidates()
-
+
await ensureLocalTracksAdded()
-
+
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
-
+
send('answer', from, { sdp: pc.localDescription })
log('⬅️ 已发送 answer 给', from)
-
+
state.value = 'in-call'
startTimer()
- }
-
- function hangup() {
+}
+
+function hangup() {
cleanup(true)
- }
-
- /* ---------------- 清理 ---------------- */
-
- function cleanup(sendToPeer) {
+}
+
+/* ---------------- 清理 ---------------- */
+
+function cleanup(sendToPeer) {
try {
- if (sendToPeer) send('hangup', peerUid.value, null)
+ if (sendToPeer) send('hangup', peerUid.value, null)
} catch {}
-
+
stopTimer()
-
+
if (pc) {
- pc.getSenders().forEach(s => s.track && s.track.stop())
- pc.close()
+ pc.getSenders().forEach(s => s.track && s.track.stop())
+ pc.close()
}
if (localStream) {
- localStream.getTracks().forEach(t => t.stop())
+ localStream.getTracks().forEach(t => t.stop())
}
-
+
pc = null
localStream = null
pendingCandidates.length = 0
-
+
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
-
+
state.value = 'idle'
- }
-
- onBeforeUnmount(() => {
+}
+
+onBeforeUnmount(() => {
cleanup(false)
stopPing()
if (ws) ws.close()
- })
- </script>
+})
+</script>
\ No newline at end of file
diff --git a/applications/drone-command/src/views/dataCockpit/components/LegendBar.vue b/applications/drone-command/src/views/dataCockpit/components/LegendBar.vue
index 52535aa..0ed4b0d 100644
--- a/applications/drone-command/src/views/dataCockpit/components/LegendBar.vue
+++ b/applications/drone-command/src/views/dataCockpit/components/LegendBar.vue
@@ -2,7 +2,7 @@
* @Author : yuan
* @Date : 2026-01-17 14:51:39
* @LastEditors : yuan
- * @LastEditTime : 2026-01-17 15:13:50
+ * @LastEditTime : 2026-01-30 15:20:56
* @FilePath : \applications\drone-command\src\views\dataCockpit\components\LegendBar.vue
* @Description :
* Copyright 2026 OBKoro1, All Rights Reserved.
@@ -12,12 +12,16 @@
<div class="legend-bar">
<div class="legend-title" data-text="图例">图例</div>
<div class="legend-item">
- <span class="legend-color defense"></span>
- <span class="legend-text" data-text="防区">防区</span>
+ <span class="legend-color detect-area"></span>
+ <span class="legend-text" data-text="侦测区">侦测区</span>
</div>
<div class="legend-item">
- <span class="legend-color partition"></span>
- <span class="legend-text" data-text="区域">区域</span>
+ <span class="legend-color alarm-area"></span>
+ <span class="legend-text" data-text="报警区">报警区</span>
+ </div>
+ <div class="legend-item">
+ <span class="legend-color key-control-area"></span>
+ <span class="legend-text" data-text="重点管制区">重点管制区</span>
</div>
<div class="legend-item">
<img class="legend-icon drone" :src="droneIcon" alt="无人机" />
@@ -84,16 +88,27 @@
border-radius: 0;
}
-.legend-color.defense {
- background: radial-gradient(50% 50% at 50% 50%, rgba(114, 243, 255, 0.45) 0%, rgba(10, 124, 136, 0.55) 100%);
- border: 1px solid rgba(114, 243, 255, 0.85);
-}
.legend-color.partition {
background: radial-gradient(50% 50% at 50% 50%, rgba(255, 215, 114, 0.5) 0%, rgba(168, 107, 0, 0.6) 100%);
border: 1px solid rgba(255, 215, 114, 0.9);
}
+.legend-color.detect-area {
+ background-color: rgba(25, 178, 230, 0.5);
+ border: 1px solid rgb(0, 251, 255);
+}
+
+.legend-color.alarm-area {
+ background-color: rgba(255, 235, 59, 0.5);
+ border: 1px solid rgb(255, 235, 59);
+}
+
+.legend-color.key-control-area {
+ background-color: rgba(247, 20, 20, 0.5);
+ border: 1px solid rgb(255, 0, 0);
+}
+
.legend-icon {
width: 30px;
height: 42px;
diff --git a/applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue b/applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue
index d983ceb..d9adc8c 100644
--- a/applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue
+++ b/applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue
@@ -175,7 +175,7 @@
const emit = defineEmits(['success'])
const formRef = ref(null)
const formData = ref(initForm())
-const visible = ref(false)
+const visible = defineModel()
const dialogMode = ref('add')
const submitting = ref(false)
const isAddChild = ref(false)
@@ -283,7 +283,6 @@
async function open({ mode, row, parent } = {}) {
dialogMode.value = mode || 'add'
- visible.value = true
isAddChild.value = mode === 'add' && !!parent?.id
await getDeptTree().then(res => {
if (deptTree) {
diff --git a/applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue b/applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue
new file mode 100644
index 0000000..793751e
--- /dev/null
+++ b/applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue
@@ -0,0 +1,169 @@
+<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="right-container" style="width: 100%">
+ <div class="header">
+ <span>设备共享</span>
+ <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
+ </div>
+
+ <div class="content">
+ <div class="command-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
+ <div class="command-table-content">
+ <el-table
+ ref="tableRef"
+ class="command-table"
+ :data="deviceList"
+ row-key="id"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" :reserve-selection="true" />
+ <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
+ <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
+ <el-table-column prop="deviceSn" show-overflow-tooltip label="设备SN" />
+ <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
+ <template v-slot="{ row }">
+ {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="belongDeptName" show-overflow-tooltip label="所属部门" />
+ </el-table>
+ </div>
+ </div>
+ </div>
+
+ <div class="footer">
+ <el-button color="#2B2B4C" @click="handleCancel">取消</el-button>
+ <el-button color="#284FE3" 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 { ref, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { nonDeptAreaDevicesApi, fwDevicePerShareSaveApi, fwDevicePerShareRemoveApi } from './fwDevicePerShareApi'
+import { getDictionaryByCode } from '@/api/system/dictbiz'
+import { getDictLabel } from '@ztzf/utils'
+
+const emit = defineEmits(['success'])
+const visible = defineModel() // 弹框显隐
+const loading = ref(false) // 加载中
+const submitting = ref(false) // 提交中
+const deviceList = ref([]) // 设备列表
+const tableRef = ref(null) // 表格实例
+const selectedRows = ref([]) // 当前选中的行
+const originalSharedIds = ref([]) // 原始已共享的设备ID集合
+const originalSharedMap = ref({}) // 原始已共享的设备ID与shareId的映射
+const currentDeptId = ref(null) // 当前操作的部门ID
+const dictObj = ref({
+ deviceType: [], // 设备类型
+})
+
+// 获取字典
+function getDictList() {
+ getDictionaryByCode('deviceType').then(res => {
+ dictObj.value = res.data.data
+ })
+}
+
+// 选中变化
+function handleSelectionChange(rows) {
+ selectedRows.value = rows
+}
+
+// 关闭弹框
+function handleCancel() {
+ visible.value = false
+}
+
+// 获取设备列表
+async function getDeviceList() {
+ loading.value = true
+ try {
+ const res = await nonDeptAreaDevicesApi({ loanToDeptId: currentDeptId.value })
+ deviceList.value = res?.data?.data ?? []
+ // 记录原始已共享的设备ID和shareId映射
+ const sharedDevices = deviceList.value.filter(item => item.isShared)
+ originalSharedIds.value = sharedDevices.map(item => item.id)
+ originalSharedMap.value = sharedDevices.reduce((map, item) => {
+ map[item.id] = item.shareId
+ return map
+ }, {})
+ // 回显已共享的设备勾选状态
+ nextTick(() => {
+ deviceList.value.forEach(item => {
+ if (item.isShared) {
+ tableRef.value?.toggleRowSelection(item, true)
+ }
+ })
+ })
+ } finally {
+ loading.value = false
+ }
+}
+
+// 提交
+async function handleSubmit() {
+ submitting.value = true
+ try {
+ const currentSelectedIds = selectedRows.value.map(item => item.id)
+
+ // 计算需要新增的设备(当前选中但原来未共享)
+ const toAddDevices = selectedRows.value.filter(item => !originalSharedIds.value.includes(item.id))
+
+ // 计算需要删除的设备(原来共享但当前未选中),使用shareId
+ const toRemoveShareIds = originalSharedIds.value
+ .filter(id => !currentSelectedIds.includes(id))
+ .map(id => originalSharedMap.value[id])
+ .filter(Boolean)
+
+ const promises = []
+
+ // 调用新增接口
+ if (toAddDevices.length > 0) {
+ const addData = toAddDevices.map(item => ({
+ deviceId: item.id,
+ deviceSn: item.deviceSn,
+ loanFromDeptId: item.belongDept,
+ loanToDeptId: currentDeptId.value,
+ }))
+ promises.push(fwDevicePerShareSaveApi(addData))
+ }
+
+ // 调用删除接口
+ if (toRemoveShareIds.length > 0) {
+ promises.push(fwDevicePerShareRemoveApi({ ids: toRemoveShareIds.join(',') }))
+ }
+
+ if (promises.length > 0) {
+ await Promise.all(promises)
+ ElMessage.success('保存成功')
+ } else {
+ ElMessage.info('未做任何修改')
+ }
+
+ visible.value = false
+ emit('success')
+ } finally {
+ submitting.value = false
+ }
+}
+
+// 打开弹框
+async function open({ row } = {}) {
+ currentDeptId.value = row?.id
+ selectedRows.value = []
+ getDictList()
+ await getDeviceList()
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js b/applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js
new file mode 100644
index 0000000..2194414
--- /dev/null
+++ b/applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js
@@ -0,0 +1,28 @@
+import request from '@/axios'
+
+// 查询非本部门区域的设备
+export const nonDeptAreaDevicesApi = params => {
+ return request({
+ url: `/drone-fw/device/fwDevice/nonDeptAreaDevices`,
+ method: 'get',
+ params,
+ })
+}
+
+// 新增设备共享
+export const fwDevicePerShareSaveApi = data => {
+ return request({
+ url: `/drone-fw/device/fwDevicePerShare/save`,
+ method: 'post',
+ data,
+ })
+}
+
+// 删除设备共享
+export const fwDevicePerShareRemoveApi = params => {
+ return request({
+ url: `/drone-fw/device/fwDevicePerShare/remove`,
+ method: 'post',
+ params,
+ })
+}
diff --git a/applications/drone-command/src/views/permissionManage/permissionDept/index.vue b/applications/drone-command/src/views/permissionManage/permissionDept/index.vue
index 9930be6..160df0b 100644
--- a/applications/drone-command/src/views/permissionManage/permissionDept/index.vue
+++ b/applications/drone-command/src/views/permissionManage/permissionDept/index.vue
@@ -62,13 +62,14 @@
<el-table-column prop="sort" show-overflow-tooltip width="80" label="排序" />
<el-table-column prop="bingId" show-overflow-tooltip width="100" label="组织id" />
<el-table-column prop="areaName" show-overflow-tooltip width="180" label="行政区划" />
-
+
<el-table-column label="操作" class-name="operation-btns" width="220">
<template v-slot="{ row }">
<el-link @click="handleView(row)">查看</el-link>
<el-link @click="handleEdit(row)">编辑</el-link>
<el-link @click="handleAddChild(row)">新增子项</el-link>
<el-link type="danger" @click="handleDelete(row)">删除</el-link>
+ <el-link type="danger" @click="handleShare(row)">共享</el-link>
</template>
</el-table-column>
</el-table>
@@ -86,18 +87,20 @@
</div>
</div>
- <FormDiaLog ref="dialogRef" @success="getList" />
+ <FormDiaLog ref="dialogRef" @success="getList" v-model="visible" v-if="visible" />
+ <ShareDiaLog ref="shareDialogRef" @success="getList" v-model="shareVisible" v-if="shareVisible" />
</basic-container>
</template>
<script setup>
import { Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
-import { onMounted, provide, ref } from 'vue'
+import { nextTick, onMounted, provide, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDictionary } from '@/api/system/dict'
import { getChildLazyTree, getDeptTree, getLazyList, remove } from '@/api/system/dept'
import { getDictLabel } from '@ztzf/utils'
import FormDiaLog from './FormDiaLog.vue'
+import ShareDiaLog from './ShareDiaLog.vue'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
@@ -110,11 +113,14 @@
const searchParams = ref(initSearchParams())
const loading = ref(true)
+const visible = ref(false)
const list = ref([])
const total = ref(0)
const selectedIds = ref([])
const queryParamsRef = ref(null)
const dialogRef = ref(null)
+const shareDialogRef = ref(null)
+const shareVisible = ref(false)
const dictObj = ref({
org_category: [],
org_nature: [],
@@ -148,19 +154,38 @@
}
function handleAdd() {
- dialogRef.value?.open({ mode: 'add' })
+ visible.value = true
+ nextTick(() => {
+ dialogRef.value?.open({ mode: 'add' })
+ })
}
function handleAddChild(row) {
- dialogRef.value?.open({ mode: 'add', parent: row })
+ visible.value = true
+ nextTick(() => {
+ dialogRef.value?.open({ mode: 'add', parent: row })
+ })
}
function handleView(row) {
- dialogRef.value?.open({ mode: 'view', row: { ...row } })
+ visible.value = true
+ nextTick(() => {
+ dialogRef.value?.open({ mode: 'view', row: { ...row } })
+ })
}
function handleEdit(row) {
- dialogRef.value?.open({ mode: 'edit', row: { ...row } })
+ visible.value = true
+ nextTick(() => {
+ dialogRef.value?.open({ mode: 'edit', row: { ...row } })
+ })
+}
+
+function handleShare(row) {
+ shareVisible.value = true
+ nextTick(() => {
+ shareDialogRef.value?.open({ row })
+ })
}
async function handleDelete(row) {
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue b/applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue
index d236c6a..33d2751 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue
@@ -6,6 +6,7 @@
@closed="visible = false"
destroy-on-close
:close-on-click-modal="false"
+ width="800px"
>
<div class="detail-container" v-if="dialogReadonly">
<el-row class="detail-row-view">
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue b/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
index e6dd00b..d68afc9 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
@@ -218,6 +218,42 @@
<!-- <el-button color="#F2F3F5" :loading="submitting" :disabled="submitting" @click="visible = false">关闭</el-button>-->
</template>
</template>
+
+ <!-- 申请弹框 -->
+ <el-dialog
+ class="gd-dialog"
+ v-model="showApplyDialog"
+ title="申请"
+ @closed="handleApplyDialogClosed"
+ width="500px"
+ destroy-on-close
+ :close-on-click-modal="false"
+ >
+ <el-form class="gd-dialog-form" ref="applyFormRef" :model="applyFormData" :rules="applyRules">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="申请对象部门" prop="applyTargetDeptId">
+ <el-tree-select
+ class="gd-select"
+ popper-class="gd-tree-select-popper"
+ v-model="applyFormData.applyTargetDeptId"
+ node-key="id"
+ :data="deptTree"
+ :props="treeProps"
+ check-strictly
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button color="#F2F3F5" @click="showApplyDialog = false">取消</el-button>
+ <el-button class="" color="#4C34FF" :loading="applying" :disabled="applying" @click="handleApplySubmit">
+ 确认
+ </el-button>
+ </template>
+ </el-dialog>
</el-dialog>
</template>
@@ -283,6 +319,14 @@
const attachmentDetailsList = ref([])
const uploading = ref(false) // 文件上传中状态
+// 申请弹框相关
+const showApplyDialog = ref(false)
+const applyFormRef = ref(null)
+const applying = ref(false)
+const applyFormData = ref({
+ applyTargetDeptId: ''
+})
+
// 表单校验规则
const rules = {
title: fieldRules(true),
@@ -292,7 +336,7 @@
catalogResourceName: fieldRules(true),
objectionDesc: fieldRules(true),
objectionBasis: fieldRules(true),
- // attachId: dialogMode.value === 'add' ? fieldRules(true) : fieldRules(false)
+ attachId: fieldRules(true)
}
@@ -300,6 +344,11 @@
const feedbackRules = {
otherObjectionDetail: fieldRules(true),
reviewOpinion: fieldRules(true),
+}
+
+// 申请弹框校验规则
+const applyRules = {
+ applyTargetDeptId: fieldRules(true),
}
// 自定义上传请求
@@ -323,9 +372,6 @@
}
// 文件移除
-// const handleFileRemove = (file, fileListVal) => {
-// uploadedFiles.value = fileListVal
-// }
const handleFileRemove = (file, fileListVal) => {
// 从展示列表中移除
fileList.value = fileListVal
@@ -375,6 +421,7 @@
// 提交数据的通用函数
async function submitForm(objectionStatus) {
+formData.value.attachId = uploadedFiles.value.length > 0 ? uploadedFiles.value.length : ''
const isValid = await formRef.value?.validate().catch(() => false)
if (!isValid) return
@@ -408,13 +455,6 @@
}),
objectionStatus: objectionStatus
}
-
- // 如果是编辑且没有修改附件,可以保留原有附件ID
- if (dialogMode.value === 'edit' && formData.value.attachIds) {
- // 这里可以根据业务需求决定是否保留原附件ID
- // 如果接口支持更新时重新上传附件ID列表,可以这样做
- }
-
await gdDataObjectionSubmitApi(submitData)
ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
visible.value = false
@@ -429,9 +469,66 @@
await submitForm('0')
}
-// 申请提交
+// 申请弹框关闭处理
+function handleApplyDialogClosed() {
+ applyFormData.value.applyTargetDeptId = ''
+}
+
+// 申请
async function handleApply() {
- await submitForm('1')
+ formData.value.attachId = uploadedFiles.value.length > 0 ? uploadedFiles.value.length : ''
+ // 先验证主表单
+ const mainFormValid = await formRef.value?.validate().catch(() => false)
+ if (!mainFormValid) return
+
+ showApplyDialog.value = true
+}
+
+// 申请提交
+async function handleApplySubmit() {
+ const isValid = await applyFormRef.value?.validate().catch(() => false)
+ if (!isValid) return
+ applying.value = true
+ try {
+ // 构建申请数据
+ const applyData = {
+ ...formData.value,
+ objectionStatus: '1',
+ applyTargetDeptId: applyFormData.value.applyTargetDeptId
+ }
+
+ // 构建符合接口要求的提交数据
+ const submitData = {
+ // 编辑模式下传递id
+ ...(applyData.id && { id: applyData.id }),
+ title: applyData.title,
+ objectionType: applyData.objectionType,
+ submitter: applyData.submitter,
+ submitterContact: applyData.submitterContact,
+ catalogResourceName: applyData.catalogResourceName,
+ objectionDesc: applyData.objectionDesc,
+ objectionBasis: applyData.objectionBasis,
+ areaCode: applyData.areaCode || '',
+ handleUnit: applyData.handleUnit || '',
+ // 构建附件列表 - 使用当前已上传的文件(包括回显的)
+ ...(uploadedFiles.value.length > 0 && {
+ attachmentList: uploadedFiles.value.map(file => ({
+ attachId: file.attachId,
+ attachName: file.originalName,
+ })),
+ }),
+ objectionStatus: applyData.objectionStatus,
+ applyTargetDeptId: applyData.applyTargetDeptId
+ }
+
+ await gdDataObjectionSubmitApi(submitData)
+ ElMessage.success('申请成功')
+ showApplyDialog.value = false
+ visible.value = false
+ emit('success')
+ } finally {
+ applying.value = false
+ }
}
// 处理异议反馈提交
@@ -509,8 +606,6 @@
ElMessage.warning('没有可下载的附件')
return
}
-
-
// 顺序下载每个附件
for (let i = 0; i < attachmentDetailsList.value.length; i++) {
const attach = attachmentDetailsList.value[i]
@@ -521,10 +616,6 @@
}
try {
- // 显示当前下载的文件信息
- const currentFileName = attach.originalName || attach.name || `附件_${i + 1}`
-
-
const response = await fetch(attach.link)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
@@ -568,8 +659,6 @@
}
}
-
-
// 打开弹框
async function open({ mode = 'add', row } = {}) {
dialogMode.value = mode
@@ -580,6 +669,8 @@
formData.value = dialogMode.value === 'add' ? initForm() : row
// 重置反馈表单数据
feedbackFormData.value = initFeedbackForm()
+ // 重置申请弹框数据
+ applyFormData.value.applyTargetDeptId = ''
if (dialogMode.value !== 'add') {
// 如果是编辑,设置ID并加载详情
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue b/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue
index fc0cc14..10a457d 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue
@@ -59,7 +59,7 @@
{{ getDictLabel(row.objectionStatus, dictObj.objectionStatus) }}
</template>
</el-table-column>
- <el-table-column prop="catalogResourceName" width="130" show-overflow-tooltip label="目录/申请资源名称" />
+ <el-table-column prop="catalogResourceName" width="140" show-overflow-tooltip label="目录/申请资源名称" />
<el-table-column prop="otherObjectionDetail" show-overflow-tooltip label="其他异议详情" />
<el-table-column prop="submitter" show-overflow-tooltip label="问题提交人" />
<el-table-column prop="submitterContact" show-overflow-tooltip width="120" label="提交人联系方式" />
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue
index 006b0ac..6f482ac 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue
@@ -68,7 +68,7 @@
</el-row>
</div>
<!-- 申请中-->
- <div v-if="detailDemandStatus === '1' && !requesterProvider">
+ <div v-if="detailDemandStatus === '1' && (permissionList.approvedBtn || permissionList.rejectBtn)">
<div class="detail-title" :style="{ marginTop:pxToRem(10)}">数据上传</div>
<div class="upload-container">
<el-upload
@@ -106,16 +106,16 @@
</div>
</div>
<template #footer>
- <template v-if="detailDemandStatus === '1' && !requesterProvider">
- <el-button color="#F2F3F5" :loading="submitting || uploading" :disabled="submitting || uploading" @click="rejectTheApplication">拒绝申请</el-button>
- <el-button class="" color="#4C34FF" :loading="submitting || uploading" :disabled="submitting || uploading" @click="approvedByTheReview">审核通过</el-button>
+ <template v-if="detailDemandStatus === '1' ">
+ <el-button v-if="permissionList.rejectBtn" color="#F2F3F5" :loading="submitting || uploading" :disabled="submitting || uploading" @click="rejectTheApplication">拒绝申请</el-button>
+ <el-button v-if="permissionList.approvedBtn" class="" color="#4C34FF" :loading="submitting || uploading" :disabled="submitting || uploading" @click="approvedByTheReview">审核通过</el-button>
</template>
<template v-if="detailDemandStatus === '2'">
<el-button class="" color="#4C34FF" @click="downloadAllFiles">全部下载</el-button>
</template>
- <template v-if="detailDemandStatus === '3'">
- <el-button class="" color="#4C34FF" @click="visible =false">关闭</el-button>
- </template>
+<!-- <template v-if="detailDemandStatus === '3'">-->
+<!-- <el-button class="" color="#4C34FF" @click="visible =false">关闭</el-button>-->
+<!-- </template>-->
</template>
</el-dialog>
@@ -142,7 +142,7 @@
import AuditRecord from '@/views/orderView/orderDataManage/supplyAdd/auditRecord.vue' // 导入新组件
import { useStore } from 'vuex'
const store = useStore()
-const requesterProvider = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
+const permission = computed(() => store.state.user.permission);
const emit = defineEmits(['success'])
const dictObj = inject('dictObj')
const deptTree = inject('deptTree')
@@ -156,6 +156,16 @@
const responseData = ref(null)
const submitting = ref(false) // 提交中状态
const uploading = ref(false) // 文件上传中状态
+function validData(value, defaultValue) {
+ return value !== undefined ? value : defaultValue
+}
+const permissionList = computed(() => {
+ return {
+ addBtn: validData(permission.value.orderData_addDemand, false),
+ rejectBtn: validData(permission.value.orderData_rejectApplication, false),
+ approvedBtn: validData(permission.value.orderData_approved, false),
+ }
+})
// 自定义上传请求函数
const handleHttpRequest = async (options) => {
uploading.value = true
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue
index d6ae8f9..9935c07 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue
@@ -8,7 +8,7 @@
:close-on-click-modal="false"
>
<div class="detail-container" v-if="dialogReadonly">
- <el-row class="detail-row-view">
+ <el-row class="detail-row-view supplyAddLabel">
<el-col :span="12">
<div class="label">需求名称</div>
<div class="val">{{ formData.demandName }}</div>
@@ -194,9 +194,45 @@
>
保存
</el-button>
- <el-button color="#4C34FF" :loading="submitting" :disabled="submitting" @click="handleApply">申请</el-button>
+ <el-button color="#4C34FF" :loading="submitting" :disabled="submitting" @click="handleApply" >申请</el-button>
</template>
+
+ <!-- 申请弹框 -->
+ <el-dialog
+ class="gd-dialog"
+ v-model="showApplyDialog"
+ title="申请"
+ @closed="handleApplyDialogClosed"
+ width="500px"
+ destroy-on-close
+ :close-on-click-modal="false"
+ >
+ <el-form class="gd-dialog-form" ref="applyFormRef" :model="applyFormData" :rules="applyRules">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="申请对象部门" prop="applyTargetDeptId">
+ <el-tree-select
+ class="gd-select"
+ popper-class="gd-tree-select-popper"
+ v-model="applyFormData.applyTargetDeptId"
+ node-key="id"
+ :data="deptTree"
+ :props="treeProps"
+ check-strictly
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button color="#F2F3F5" @click="showApplyDialog = false">取消</el-button>
+ <el-button class="" color="#4C34FF" :loading="applying" :disabled="applying" @click="handleApplySubmit">
+ 确认
+ </el-button>
+ </template>
+ </el-dialog>
</el-dialog>
</template>
@@ -240,6 +276,14 @@
const dialogReadonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
+// 申请弹框相关
+const showApplyDialog = ref(false)
+const applyFormRef = ref(null)
+const applying = ref(false)
+const applyFormData = ref({
+ applyTargetDeptId: ''
+})
+
const rules = {
demandName: fieldRules(true),
demandType: fieldRules(true),
@@ -253,6 +297,11 @@
responsibleDeptId: fieldRules(true),
dataSource: fieldRules(true),
demandInfo: fieldRules(true),
+}
+
+// 申请弹框校验规则
+const applyRules = {
+ applyTargetDeptId: fieldRules(true),
}
// 根据部门ID获取部门名称
function getDeptNameById(deptId, deptList) {
@@ -295,19 +344,37 @@
}
}
-// 申请提交
+// 申请弹框关闭处理
+function handleApplyDialogClosed() {
+ applyFormData.value.applyTargetDeptId = ''
+}
+// 申请
async function handleApply() {
- const isValid = await formRef.value?.validate().catch(() => false)
+ // 先验证主表单
+ const mainFormValid = await formRef.value?.validate().catch(() => false)
+ if (!mainFormValid) return
+
+ showApplyDialog.value = true
+ }
+// 申请提交
+async function handleApplySubmit() {
+ const isValid = await applyFormRef.value?.validate().catch(() => false)
if (!isValid) return
- submitting.value = true
+ applying.value = true
try {
- formData.value.demandStatus = '1'
- await gdSupplyDemandSubmitApi(formData.value)
+ // 构建申请数据
+ const applyData = {
+ ...formData.value,
+ demandStatus: '1',
+ applyTargetDeptId: applyFormData.value.applyTargetDeptId
+ }
+ await gdSupplyDemandSubmitApi(applyData)
ElMessage.success('申请成功')
+ showApplyDialog.value = false
visible.value = false
emit('success')
} finally {
- submitting.value = false
+ applying.value = false
}
}
@@ -334,3 +401,10 @@
defineExpose({ open })
</script>
+<style scoped lang="scss">
+.supplyAddLabel {
+ .label {
+ width: 120px !important;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue
index 76d54e2..ac44ba2 100644
--- a/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue
+++ b/applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue
@@ -35,7 +35,7 @@
</el-form-item>
</el-form>
- <div class="gd-table-toolbar">
+ <div class="gd-table-toolbar" v-if="permissionList.addBtn">
<el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">供需填报新增</el-button>
</div>
@@ -79,9 +79,8 @@
<el-table-column label="操作" class-name="operation-btns" width="150">
<template v-slot="{ row }">
<el-link type="primary" @click="openForm('view', row)">查看</el-link>
- <!-- 只对非审批状态显示编辑和删除按钮 -->
- <el-link type="primary" @click="openForm('edit', row)" v-if="!['1', '2', '3'].includes(row.demandStatus)">编辑</el-link>
- <el-link type="primary" @click="handleDelete(row)" v-if="!['1', '2', '3'].includes(row.demandStatus)">删除</el-link>
+ <el-link type="primary" @click="openForm('edit', row)" v-if="!['1', '2', '3'].includes(row.demandStatus) && permissionList.addBtn">编辑</el-link>
+ <el-link type="primary" @click="handleDelete(row)" v-if="!['1', '2', '3'].includes(row.demandStatus) && permissionList.deleteBtn">删除</el-link>
</template>
</el-table-column>
</el-table>
@@ -116,7 +115,9 @@
gdSupplyDemandPageApi,
gdSupplyDemandRemoveApi,
} from '@/views/orderView/orderDataManage/supplyAdd/supplyAddApi'
-
+import { useStore } from 'vuex'
+const store = useStore()
+const permission = computed(() => store.state.user.permission);
// 初始化查询参数
const initSearchParams = () => ({
demandName: '', // 需求名称
@@ -151,6 +152,17 @@
provide('detailDemandStatus', detailDemandStatus)
const reasonForRejection = ref('') //拒绝原因
provide('reasonForRejection', reasonForRejection)
+
+function validData(value, defaultValue) {
+ return value !== undefined ? value : defaultValue
+}
+const permissionList = computed(() => {
+ return {
+ addBtn: validData(permission.value.orderData_addDemand, false),
+ deleteBtn: validData(permission.value.orderData_delete, false),
+ }
+})
+
// 根据部门ID获取部门名称
function getDeptNameById(deptId, deptList) {
if (!deptId || !Array.isArray(deptList)) return ''
diff --git a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue
index 50b19e9..08b4a6f 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue
@@ -2,7 +2,7 @@
<el-dialog
class="gd-dialog"
v-model="visible"
- title="编辑"
+ :title="titleEnum[dialogMode]"
@closed="visible = false"
destroy-on-close
:close-on-click-modal="false"
diff --git a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
index 2c62033..b2fa57a 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -205,34 +205,35 @@
</div>
<template #footer>
- <template v-if="requester">
- <el-button v-if="['1', '4'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
- {{ gdStatusObj[taskStatus].reason }}
- </el-button>
- <el-button @click="statusChange(3)" v-if="taskStatus === '3'" color="#4C34FF">撤回任务</el-button>
+ <el-button v-if="['1', '4', '7'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
+ {{ gdStatusObj[taskStatus].reason }}
+ </el-button>
+ <el-button @click="statusChange(3)" v-if="taskStatus === '3' && permission.flyOrder_revoke" color="#4C34FF">
+ 撤回任务
+ </el-button>
+
+ <template v-if="permission.flyOrder_controlAcceptance">
<el-button @click="refusalToAccept" v-if="taskStatus === '6'" color="#F2F3F5">拒绝验收</el-button>
<el-button @click="statusChange(6)" v-if="taskStatus === '6'" color="#4C34FF">验收通过</el-button>
- <el-button
- v-if="!dialogReadonly || ['1', '4'].includes(taskStatus)"
- :loading="submitting"
- :disabled="submitting"
- color="#4C34FF"
- @click="handleSubmit"
- >
- 提交
- </el-button>
</template>
- <template v-else>
- <el-button v-if="['7'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
- {{ gdStatusObj[taskStatus].reason }}
- </el-button>
+ <el-button
+ v-if="!dialogReadonly || (['1', '4'].includes(taskStatus) && permission.flyOrder_add)"
+ :loading="submitting"
+ :disabled="submitting"
+ color="#4C34FF"
+ @click="handleSubmit"
+ >
+ 提交
+ </el-button>
+ <template v-if="permission.flyOrder_controlSignFor">
<el-button v-if="taskStatus === '0'" @click="addDescription" color="#F2F3F5">拒绝签收</el-button>
<el-button v-if="taskStatus === '0'" @click="statusChange(1)" color="#4C34FF">签收</el-button>
+ </template>
+ <template v-if="permission.flyOrder_controlReview">
<el-button v-if="taskStatus === '3'" @click="addDescription" color="#F2F3F5">驳回</el-button>
<el-button v-if="taskStatus === '3'" @click="statusChange(4)" color="#4C34FF">同意</el-button>
</template>
</template>
-
<RefuseOrderDialog1
ref="refuseOrderDialogRef"
v-if="rejectVisible"
@@ -252,11 +253,11 @@
import { pxToRem } from '@/utils/rem'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { gdTaskResultListApi } from '@/views/orderView/orderManage/clueEvents/achievementApi'
-import RefuseOrderDialog from '@/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue'
import RefuseOrderDialog1 from '@/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue'
import { Check } from '@element-plus/icons-vue'
import { queryAirById } from '@/api/zkxt'
import * as Cesium from 'cesium'
+import { useStore } from 'vuex'
// 初始化表单数据
const initForm = () => ({
@@ -270,11 +271,12 @@
taskDesc: '',
workOrderId: null,
})
+const store = useStore()
+const permission = computed(() => store.state.user.permission)
const rejectVisible = ref(false)
const mapRef = ref(null)
const dictObj = inject('dictObj')
-const requester = inject('requester')
const emit = defineEmits(['success', 'refusalAccept'])
const formRef = ref(null)
const formData = ref(initForm())
@@ -372,7 +374,7 @@
// 获取飞手列表
async function getFlyerList() {
- const res = await gdFlyerPageApi({ size: 999,current: 1 })
+ const res = await gdFlyerPageApi({ size: 999, current: 1 })
flyerList.value = res?.data?.data?.records ?? []
}
@@ -429,7 +431,14 @@
} finally {
}
}
-
+import { ArrowLineMaterialProperty } from '@/utils/cesium/Material'
+let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
+ color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
+ directionColor: new Cesium.Color(1, 1, 1, 1),
+ outlineColor: new Cesium.Color(1, 1, 1, 1),
+ outlineWidth: 0,
+ speed: 5,
+})
// 获取航线详情
function getAirDetails() {
queryAirById(formData.value.patrolRouteUrl).then(res => {
@@ -440,8 +449,8 @@
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(result),
clampToGround: true,
- width: 3,
- material: Cesium.Color.RED,
+ width: 4,
+ material: arrowLineMaterialProperty,
},
})
// 渲染起点(蓝色)
@@ -514,23 +523,22 @@
</script>
<style lang="scss" scoped>
-
/* 时间线样式 */
:deep(.gd-timeline) {
- padding-left: 90px;
+ padding-left: 90px;
}
:deep(.el-timeline-item) {
- padding-bottom: 20px;
- .item-content {
- position: relative;
- .flowName {
- width: 80px;
- position: absolute;
- left: -120px;
- top: 0px;
- }
- }
+ padding-bottom: 20px;
+ .item-content {
+ position: relative;
+ .flowName {
+ width: 80px;
+ position: absolute;
+ left: -120px;
+ top: 0px;
+ }
+ }
}
.imgBox {
display: flex;
@@ -547,8 +555,8 @@
}
}
:deep(.el-timeline-item__timestamp) {
- font-size: 12px;
- color: #999;
- margin-top: 2px;
+ font-size: 12px;
+ color: #999;
+ margin-top: 2px;
}
</style>
diff --git a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue
index 08fcba2..bfaf131 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue
@@ -72,7 +72,7 @@
</el-form>
<div class="gd-table-toolbar">
- <el-button v-if="requester" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增</el-button>
+ <el-button v-if="permission.flyOrder_add" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">拆分工单</el-button>
</div>
<div class="gd-table-container" v-loading="loading">
@@ -139,7 +139,8 @@
import { airlineListApi } from '@/api/zkxt'
const store = useStore()
-const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
+const permission = computed(() => store.state.user.permission)
+
// 初始化查询参数
const initSearchParams = () => ({
patrolTaskName: '', // 任务名称
@@ -169,7 +170,6 @@
})
const activeName = ref('all')
provide('dictObj', dictObj)
-provide('requester', requester)
// 获取列表
async function getList() {
@@ -222,7 +222,7 @@
function viewDiaLogView(row) {
viewDiaLogVisible.value = true
nextTick(() => {
- let mode = ['1', '4'].includes(row.taskStatus) && requester.value ? 'edit' : 'view'
+ let mode = ['1', '4'].includes(row.taskStatus) && permission.value.flyOrder_add ? 'edit' : 'view'
viewDiaLogRef.value?.open({ mode, row })
})
}
diff --git a/applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue b/applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
index 94c76cd..a9e3736 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
@@ -176,64 +176,80 @@
</div>
<div class="rightBox" v-if="!dialogReadonly">
<div class="detail-title">推荐设备</div>
- <template v-if="formData.deviceLoadDemand && pointList.length">
- <el-table
- ref="deviceTableRef"
- :data="deviceList"
- row-key="id"
- @selection-change="handleDeviceSelectionChange"
- >
- <el-table-column type="selection" width="55" :reserve-selection="true" />
- <el-table-column prop="nickname" label="设备名称" />
- <el-table-column prop="deviceName" label="设备型号" />
- <el-table-column prop="devicePayload" label="设备负载" />
- </el-table>
- </template>
- <div v-else class="tipSelect">请先选择设备需求并在地图上绘制工单范围</div>
+ <el-table
+ :empty-text="(formData.deviceLoadDemand && pointList.length) ? '暂无数据' : '请先选择设备需求并在地图上绘制工单范围'"
+ ref="deviceTableRef"
+ :data="(formData.deviceLoadDemand && pointList.length) ? deviceList : []"
+ row-key="id"
+ @selection-change="handleDeviceSelectionChange"
+ >
+ <el-table-column type="selection" width="55" :reserve-selection="true" />
+ <el-table-column prop="nickname" label="设备名称" />
+ <el-table-column prop="deviceName" label="设备型号" />
+ <el-table-column prop="devicePayload" label="设备负载" />
+ </el-table>
</div>
</div>
<template #footer>
- <template v-if="requester">
- <template v-if="!suddenlyEdit">
- <el-button
- v-if="['11', '21', '23', '24', '25', '31', '60'].includes(gdStatus)"
- @click="viewDescription"
- color="#F2F3F5"
- >
- {{ gdStatusObj[gdStatus].reason }}
- </el-button>
- <el-button @click="addDescription" v-if="['20', '24', '25'].includes(gdStatus)" color="#F2F3F5">
- 申请取消
- </el-button>
- <el-button color="#4C34FF" @click="requestModification" v-if="['20', '25'].includes(gdStatus)">
- 申请修改
- </el-button>
- </template>
+ <el-button
+ v-if="['11', '21', '23','31', '60'].includes(gdStatus)"
+ @click="viewDescription"
+ color="#F2F3F5"
+ >
+ {{ gdStatusObj[gdStatus].reason }}
+ </el-button>
+
+ <template v-if="!suddenlyEdit">
<el-button
- v-if="['11'].includes(gdStatus) || suddenlyEdit || (!dialogReadonly && !formData.id)"
- :loading="submitting"
- :disabled="submitting"
- color="#4C34FF"
- @click="handleSubmit"
+ @click="addDescription"
+ v-if="['20'].includes(gdStatus) && permission.order_applyCancel"
+ color="#F2F3F5"
>
- {{ gdStatus === '11' || !gdStatus ? '发布' : '提交修改' }}
+ 申请取消
+ </el-button>
+ <el-button
+ color="#4C34FF"
+ @click="requestModification"
+ v-if="['20'].includes(gdStatus) && permission.order_applyNegotiation"
+ >
+ 申请修改
</el-button>
</template>
- <template v-else>
- <el-button v-if="['21', '23'].includes(gdStatus)" @click="viewDescription" color="#F2F3F5">
- {{ gdStatusObj[gdStatus].reason }}
- </el-button>
- <el-button v-if="gdStatus === '50'" @click="addDescription" color="#F2F3F5">结算</el-button>
+ <el-button
+ v-if="(['11'].includes(gdStatus) || suddenlyEdit || !formData.id) && permission.order_release"
+ :loading="submitting"
+ :disabled="submitting"
+ color="#4C34FF"
+ @click="handleSubmit"
+ >
+ {{ gdStatus === '11' || !gdStatus ? '发布' : '提交修改' }}
+ </el-button>
+ <el-button v-if="gdStatus === '50' && permission.order_settlement" @click="addDescription" color="#F2F3F5">
+ 结算
+ </el-button>
+ <template v-if="permission.order_controlOrder">
<el-button v-if="gdStatus === '10'" @click="addDescription" color="#F2F3F5">拒绝接单</el-button>
<el-button v-if="gdStatus === '10'" @click="statusChange(1)" color="#4C34FF">接单</el-button>
- <el-button v-if="gdStatus === '22'" @click="addDescription" color="#F2F3F5">不同意修改</el-button>
- <el-button v-if="gdStatus === '22'" @click="statusChange(7)" color="#4C34FF">同意修改</el-button>
- <el-button v-if="gdStatus === '21'" @click="addDescription" color="#F2F3F5">不同意取消</el-button>
- <el-button v-if="gdStatus === '21'" @click="statusChange(5)" color="#4C34FF">同意取消</el-button>
- <el-button v-if="gdStatus === '30'" @click="addDescription" color="#4C34FF">协商修改</el-button>
- <el-button v-if="gdStatus === '31'" @click="statusChange(10)" color="#4C34FF">同意</el-button>
</template>
+ <template v-if="permission.order_controlEdit">
+ <el-button v-if="gdStatus === '22'" @click="statusChange(8)" color="#F2F3F5">驳回</el-button>
+ <el-button v-if="gdStatus === '22'" @click="statusChange(7)" color="#4C34FF">同意</el-button>
+ </template>
+ <template v-if="permission.order_controlCancel">
+ <el-button v-if="gdStatus === '21'" @click="statusChange(6)" color="#F2F3F5">驳回</el-button>
+ <el-button v-if="gdStatus === '21'" @click="statusChange(5)" color="#4C34FF">同意</el-button>
+ </template>
+ <el-button v-if="gdStatus === '30' && permission.order_applyNegotiation" @click="addDescription" color="#4C34FF">
+ 协商修改
+ </el-button>
+ <el-button
+ v-if="gdStatus === '31' && permission.order_controlNegotiation"
+ @click="statusChange(10)"
+ color="#4C34FF"
+ >
+ 同意
+ </el-button>
</template>
<RefuseOrderDialog
@@ -246,7 +262,7 @@
</template>
<script setup>
-import { computed, ref, onMounted, nextTick } from 'vue'
+import { computed, ref, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { dateRangeFormat, fieldRules, flyVisual, geomAnalysis, getDictLabel } from '@ztzf/utils'
import {
@@ -265,6 +281,12 @@
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { gdPatrolTaskPageApi } from '@/views/orderView/orderManage/inspectionRequest/inspectionRequestApi'
import { Check } from '@element-plus/icons-vue'
+import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
+import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
+import { useStore } from 'vuex'
+const store = useStore()
+const permission = computed(() => store.state.user.permission)
+
// 初始化表单数据
const initForm = () => ({
workOrderName: '',
@@ -297,32 +319,25 @@
let viewer
let drawPolygonExample
let pointList = ref([])
-const requester = inject('requester')
const suddenlyEdit = ref(false)
const processList = ref([])
let viewPlane
const hasPatrolTaskList = computed(() => ['30', '40', '50', '60'].includes(String(formData.value.workOrderStatus)))
-import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
-import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
const gdStatusObj = {
- '10': { reason: '拒绝原因', operationType: '2' },
- '11': { reason: '拒绝原因' },
+ '10': { reason: '驳回原因', operationType: '2' },
+ '11': { reason: '驳回原因' },
'20': { reason: '取消原因', operationType: '3' },
- '21': { reason: '原因', operationType: '6' },
- '22': { reason: '驳回原因', operationType: '8' },
'23': { reason: '取消原因' },
- '24': { reason: '驳回原因', operationType: '3' },
- '25': { reason: '驳回原因', operationType: '3' },
- '30': { reason: '修改原因', operationType: '9' },
- '31': { reason: '修改原因' },
+ '30': { reason: '协商修改原因', operationType: '9' },
+ '31': { reason: '协商修改原因' },
'40': { reason: '' },
'50': { reason: '情况说明', operationType: '11' },
'60': { reason: '情况说明' },
}
// 工单状态:0草稿、10发布中_接单中、11发布中_拒绝接单、20响应中_待拆分、21响应中_申请取消、22响应中_申请修改、23响应中_已取消、
-// 24响应中_拒绝取消、25响应中_拒绝修改 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、50验收通过_待结算、60结算完成_已结算
+// 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、50验收通过_待结算、60结算完成_已结算
const gdStatus = computed(() => formData.value?.workOrderStatus)
const rules = {
@@ -595,9 +610,7 @@
const image = entity.billboard?.image?._value
if ((image === droneIcon || image === droneActiveIcon) && entity.label) {
const device = deviceList.value.find(d => d.nickname === entity.label.text._value)
- entity.billboard.image = device && selectedIds.includes(String(device.id))
- ? droneActiveIcon
- : droneIcon
+ entity.billboard.image = device && selectedIds.includes(String(device.id)) ? droneActiveIcon : droneIcon
}
})
}
@@ -622,6 +635,18 @@
</script>
<style lang="scss" scoped>
+.el-table{
+ height: 0;
+ flex: 1;
+ :deep(){
+ .el-scrollbar__view{
+ height: 100%;
+ }
+ .el-table__empty-text{
+ line-height: normal;
+ }
+ }
+}
.content {
display: flex;
gap: 0 20px;
@@ -643,6 +668,8 @@
.rightBox {
width: 350px;
+ display: flex;
+ flex-direction: column;
.title {
font-weight: 500;
font-size: 14px;
diff --git a/applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue b/applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue
index adfa2b9..4a28286 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue
@@ -2,7 +2,7 @@
<el-dialog
class="gd-dialog"
v-model="visible"
- title="编辑"
+ :title="titleEnum[dialogMode]"
@closed="visible = false"
destroy-on-close
:close-on-click-modal="false"
diff --git a/applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue b/applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
index 7864d2f..da197d7 100644
--- a/applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
+++ b/applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
@@ -72,17 +72,31 @@
</el-form>
<div class="gd-table-toolbar">
-<!-- <el-button color="#F2F3F5" @click="requester = !requester">我是{{ requester ? '需求方' : '服务方' }}</el-button>-->
-<!-- <el-button color="#F2F3F5" @click="previewVisible = true">预览</el-button>-->
- <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')" v-if="requester">新增</el-button>
- <el-button :icon="Delete" color="#4C34FF" :disabled="!selectedIds.length" @click="handleDelete()" v-if="requester">删除</el-button>
+ <el-button
+ v-if="permission.order_release"
+ :icon="Plus"
+ color="#4C34FF"
+ type="primary"
+ @click="openForm('add')"
+ >
+ 新增
+ </el-button>
+<!-- <el-button
+ :icon="Delete"
+ color="#4C34FF"
+ :disabled="!selectedIds.length"
+ @click="handleDelete()"
+ v-if="permission.order_delete"
+ >
+ 删除
+ </el-button>-->
</div>
<div class="gd-table-container" v-loading="loading">
<div class="gd-table-content gd-table-content-bg">
<el-table class="gd-table" :data="list" @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="46" v-if="requester"/>
+ <el-table-column type="selection" width="46" v-if="permission.order_delete" />
<el-table-column type="index" width="64" label="序号" />
<el-table-column prop="workOrderName" show-overflow-tooltip label="工单名称" />
<el-table-column prop="workOrderCode" show-overflow-tooltip label="工单编号" />
@@ -112,9 +126,9 @@
<el-table-column prop="createTime" show-overflow-tooltip label="工单创建时间" />
<el-table-column label="操作" class-name="operation-btns">
<template v-slot="{ row }">
- <el-link type="primary" @click="openFormChange(row)">查看</el-link>
+ <el-link type="primary" @click="openFormChange(row)">查看</el-link>
<!--<el-link type="primary" @click="openForm('edit', row)">编辑</el-link>-->
- <el-link type="primary" @click="handleDelete(row)" v-if="requester">删除</el-link>
+<!-- <el-link type="primary" @click="handleDelete(row)" v-if="permission.order_delete">删除</el-link>-->
</template>
</el-table-column>
</el-table>
@@ -132,7 +146,7 @@
</div>
</div>
- <PreviewFiles v-model="previewVisible" :src="testDock" type="docx"/>
+ <PreviewFiles v-model="previewVisible" :src="testDock" type="docx" />
<FormDiaLog ref="dialogRef" @success="getList" v-if="dialogVisible" v-model="dialogVisible" />
</basic-container>
</template>
@@ -151,8 +165,10 @@
import { useStore } from 'vuex'
const store = useStore()
-const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
const activeName = ref('all')
+
+const permission = computed(() => store.state.user.permission)
+console.log(permission.value, 'permission')
// 初始化查询参数
const initSearchParams = () => ({
@@ -164,7 +180,6 @@
current: 1, // 当前页
size: 10, // 每页大小
})
-// const requester = ref(true)
const searchParams = ref(initSearchParams()) // 查询参数
const dateRange = ref([]) // 执行时间范围
const total = ref(0) // 总条数
@@ -181,8 +196,6 @@
workOrderStatus: [], // 工单状态
})
provide('dictObj', dictObj)
-provide('requester', requester)
-
// 获取列表
async function getList() {
@@ -230,13 +243,13 @@
})
}
// 工单状态:0草稿、10发布中_接单中、11发布中_拒绝接单、20响应中_待拆分、
-// 21响应中_申请取消、22响应中_申请修改、23响应中_已取消、24响应中_拒绝取消、
-// 25响应中_拒绝修改 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、
+// 21响应中_申请取消、22响应中_申请修改、23响应中_已取消、
+// 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、
// 50验收通过_待结算、60结算完成_已结算
function openFormChange(row) {
dialogVisible.value = true
nextTick(() => {
- const mode = ['11'].includes(row.workOrderStatus) && requester.value ? 'edit' : 'view'
+ const mode = ['11'].includes(row.workOrderStatus) && permission.value.order_release ? 'edit' : 'view'
dialogRef.value?.open({ mode, row })
})
}
diff --git a/packages/constants/areaStyles.js b/packages/constants/areaStyles.js
new file mode 100644
index 0000000..bcf6093
--- /dev/null
+++ b/packages/constants/areaStyles.js
@@ -0,0 +1,36 @@
+import * as Cesium from 'cesium'
+
+export const AREA_TYPE_STYLE_MAP = {
+ '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),
+ },
+}
+
+export const DEFAULT_AREA_STYLE = {
+ fill: Cesium.Color.fromBytes(45, 140, 240, 99),
+ outline: Cesium.Color.fromBytes(45, 140, 240, 255),
+}
+
+export const BUFFER_LEVEL_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),
+ },
+]
diff --git a/packages/constants/index.js b/packages/constants/index.js
index 05fd3f4..24549e7 100644
--- a/packages/constants/index.js
+++ b/packages/constants/index.js
@@ -6,3 +6,4 @@
export * from './publicEnums';
export * from './workEnums';
export * from './fwDevice';
+export * from './areaStyles';
diff --git a/uniapps/work-app/src/pages/login/index.vue b/uniapps/work-app/src/pages/login/index.vue
index 9e37a79..dfd006d 100644
--- a/uniapps/work-app/src/pages/login/index.vue
+++ b/uniapps/work-app/src/pages/login/index.vue
@@ -82,8 +82,8 @@
});
}
const loginFormGd = ref({
- username: "admin",
- password: "admin",
+ username: "zhx",
+ password: "123456",
});
async function submit() {
diff --git a/uniapps/work-app/src/subPackages/workDetail/index.vue b/uniapps/work-app/src/subPackages/workDetail/index.vue
index 71ed67e..1244b81 100644
--- a/uniapps/work-app/src/subPackages/workDetail/index.vue
+++ b/uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -4,7 +4,8 @@
<!-- 自定义导航栏 -->
<u-navbar title="工单详情" :is-back="true" back-text="" :back-icon-size="40" @left-click="onBackClick">
<template #right>
- <div class="share-btn" @click="onShareClick"></div>
+ <div v-if="isH5" class="tapShare-icon"><img src="/static/images/work/share.svg" @click="onShareClick"/></div>
+ <div v-else class="share-btn" @click="onShareClick"></div>
</template>
</u-navbar>
<div class="detailTop">
@@ -71,7 +72,7 @@
<up-button type="primary" color="#1D6FE9" text="确认" @click="confirmTheTicket"></up-button>
</div>
<div class="btngroups" v-if="workDetailData.eventStatus === 3">
- <up-button type="primary" color="#1D6FE9" text="已确认" ></up-button>
+ <up-button type="primary" color="#1D6FE9" text="已确认" @click="confirmedHandle"></up-button>
</div>
</div>
<!-- 分享弹出层 -->
@@ -102,7 +103,6 @@
<script setup>
// import { getShowImg, getSmallImg } from '@/utils/util'
-import { ref, computed } from 'vue'
import {getGddetailedData,backGdApi} from '/src/api/work/index.js'
import dayjs from 'dayjs'
const formatDate = dateString => {
@@ -111,9 +111,12 @@
const eventNum = ref('')
// 工单内容
const workDetailData = ref({})
+// 环境判断
+const isH5 = ref(false)
onLoad(async options => {
eventNum.value = options.id
await getDataList(options.id)
+ checkEnvironment()
})
const getDataList = async val => {
const params = {
@@ -122,6 +125,19 @@
const res = await getGddetailedData(params)
const response = res.data.data
workDetailData.value = response
+}
+// 检查运行环境
+const checkEnvironment = () => {
+ const systemInfo = uni.getSystemInfoSync()
+ // #ifdef APP-PLUS
+ isH5.value = false
+ // #endif
+
+ // #ifdef H5
+ isH5.value = true
+ // #endif
+
+ console.log('当前环境:', isH5.value ? 'H5' : 'App')
}
// 图片预览
const previewImage = index => {
@@ -180,6 +196,10 @@
uni.navigateBack()
})
})
+}
+// 已确认
+const confirmedHandle = () => {
+ uni.navigateBack()
}
// 分享模态框状态
const showShareModal = ref(false)
@@ -312,12 +332,22 @@
}
} // 分享按钮样式
.share-btn {
+ // #ifdef APP-PLUS
width: 40rpx;
height: 40rpx;
background-image: url('/static/images/work/share.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
+ // #endif
+ }
+ .tapShare-icon {
+ width: 40rpx;
+ height: 40rpx;
+ img {
+ width: 40rpx;
+ height: 40rpx;
+ }
}
.detailTop {
// #ifdef APP-PLUS
--
Gitblit v1.9.3