吉安感知网项目-前端
chenyao
2026-02-02 1731a7c28db644bb862eb54089d15fe3991fd27c
Merge remote-tracking branch 'origin/master'
32 files modified
1 files renamed
14 files deleted
7 files added
8015 ■■■■ changed files
.npmrc 2 ●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/api/dataCockpit/index.js 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue 304 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/DrawPolygon.js 817 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js 37 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js 35 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/index.js 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js 44 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 70 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/1.json patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue 241 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/precinctInfoApi.js 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue 11 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue 9 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/fwDevice.js 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/folderFile.vue 316 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/leftList.vue 588 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/nationalSpatialPlanning.vue 49 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/rightEdit.vue 471 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/utils.js 619 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/index.vue 604 ●●●●● patch | view | raw | blame | history
applications/task-work-order/env/.env 2 ●●●●● patch | view | raw | blame | history
applications/task-work-order/env/.env.development 2 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/PlaybackVideo/PlaybackVideo.vue 141 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/PlaybackVideo/components/MapContainer.vue 227 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/PlaybackVideo/components/PhotoList.vue 187 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/PlaybackVideo/components/VideoPlayer.vue 170 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/map-container/mapContainer.vue 172 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/main.js 2 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/router/views/index.js 25 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/publicCesium.js 80 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/inventory/index.vue 20 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/inventory/接口文档.txt 105 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/materials/FormDiaLog.vue 303 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/materials/index.vue 230 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/materials/materialsApi.js 46 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/appConfiguration/materials/接口文档.txt 321 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue 1 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/dataObjectionApi.js 9 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue 6 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue 7 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 7 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue 20 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue 3 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/resource/components/spotDetails.vue 955 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/resource/patchManagement.vue 569 ●●●●● patch | view | raw | blame | history
packages/utils/map/DrawPolygon.js 43 ●●●●● patch | view | raw | blame | history
packages/utils/map/index.js 45 ●●●●● patch | view | raw | blame | history
packages/utils/package.json 36 ●●●●● patch | view | raw | blame | history
pnpm-lock.yaml 9 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/workDetail/index.vue 10 ●●●● patch | view | raw | blame | history
.npmrc
@@ -2,7 +2,7 @@
//center.javablade.com/api/packages/blade/npm/:_authToken=66c6549d47896da834ef4a84b5fcbf4763744b28
registry="https://registry.npmmirror.com"
#依赖开启严格模式
strict-peer-dependencies=true
strict-peer-dependencies=false
# 自动安装对等依赖,无需手动安装
auto-install-peers=true
# 允许所有包执行构建脚本(避免新依赖安装后无法运行)pnpm9的坑
applications/drone-command/env/.env.development
@@ -2,7 +2,7 @@
 # @Author       : yuan
 # @Date         : 2026-01-31 14:34:31
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-31 15:18:17
 # @LastEditTime : 2026-02-02 11:01:46
 # @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.33
VITE_APP_URL=http://192.168.1.168
#新大屏地址
VITE_APP_DASHBOARD_URL='https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/api/dataCockpit/index.js
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-09 11:14:04
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-26 19:02:07
 * @LastEditTime : 2026-02-02 09:45:21
 * @FilePath     : \applications\drone-command\src\api\dataCockpit\index.js
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -83,6 +83,15 @@
    })
}
// 最新 --- 数据驾驶舱地图聚合
export const newCockpitAggregationApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/statisticalDeviceOut',
        method: 'get',
        params
    })
}
// 区域列表
export const areaDivideListApi = (params) => {
    return request({
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -53,7 +53,7 @@
import { fwDefenseSceneManageListApi } from '@/views/areaManage/sceneManage/sceneManageApi'
import { cockpitAggregationApi } from '@/api/dataCockpit'
import { newCockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
import offlineEquipmentIcon from '@/assets/images/dataCockpit/map/offline-equipment.png'
@@ -77,6 +77,7 @@
const CLUSTER_HEIGHT = 100000
const DETAIL_HEIGHT = 10000
const POLYGON_HEIGHT_M = 0.2
const DRONE_TRACK_DURATION_S = 30 * 60
const props = defineProps({
    allDevices: {
@@ -119,17 +120,21 @@
let droneTrackBillboardCollection = null
let droneTrackPolylineCollection = null
let droneTrackTickHandler = null
let droneTrackRafId = null
let droneTrackStartTime = null
let droneTrackAnimStartAt = 0
let droneTrackLastTickAt = 0
let droneTrackRuntime = []
let droneTrackSource = null
let mapReadyHandled = false
let favoritePulseRafId = null
let favoritePulseStartAt = 0
let favoritePulseElapsed = 0
const favoritePulseEntities = []
const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
const PULSE_MIN_RADIUS_M = 60
const PULSE_MAX_RADIUS_M = 180
const PULSE_DURATION_S = 1.6
const PULSE_MIN_RADIUS_M = 2
const PULSE_MAX_RADIUS_M = 30
const PULSE_DURATION_S = 1.4
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
@@ -212,27 +217,105 @@
    return canvas
}
class PulseSphereMaterialProperty {
    constructor(color, speed = 1.2, bands = 3.0, softness = 0.18) {
        this._definitionChanged = new Cesium.Event()
        this.color = color
        this.speed = speed
        this.bands = bands
        this.softness = softness
    }
    get isConstant() {
        return true
    }
    get definitionChanged() {
        return this._definitionChanged
    }
    getType() {
        return 'PulseSphereMaterial'
    }
    getValue(_time, result = {}) {
        result.color = this.color
        result.time = (performance.now() / 1000) * this.speed
        result.bands = this.bands
        result.softness = this.softness
        return result
    }
    equals(other) {
        return (
            other instanceof PulseSphereMaterialProperty &&
            Cesium.Color.equals(this.color, other.color) &&
            this.speed === other.speed &&
            this.bands === other.bands &&
            this.softness === other.softness
        )
    }
}
let pulseSphereMaterial = null
const getPulseSphereMaterial = () => {
    if (pulseSphereMaterial) return pulseSphereMaterial
    Cesium.Material._materialCache.addMaterial('PulseSphereMaterial', {
        fabric: {
            type: 'PulseSphereMaterial',
            uniforms: {
                color: pulseBaseColor.withAlpha(0.45),
                time: 0,
                bands: 3.0,
                softness: 0.18,
            },
            source: `
                czm_material czm_getMaterial(czm_materialInput materialInput)
                {
                    czm_material material = czm_getDefaultMaterial(materialInput);
                    vec3 normalEC = normalize(materialInput.normalEC);
                    vec3 viewEC = normalize(-materialInput.positionToEyeEC);
                    float ndv = max(dot(normalEC, viewEC), 0.0);
                    float fresnel = pow(1.0 - ndv, 1.6);
                    float wave = sin((ndv * bands + time) * 6.2831853);
                    float ring = smoothstep(-softness, softness, wave);
                    float alpha = (ring * 0.6 + fresnel * 0.4) * color.a;
                    material.diffuse = color.rgb;
                    material.alpha = alpha;
                    return material;
                }
            `,
        },
        translucent: () => true,
    })
    pulseSphereMaterial = new PulseSphereMaterialProperty(pulseBaseColor.withAlpha(0.45), 1.1, 3.2, 0.22)
    return pulseSphereMaterial
}
const createFavoritePulseEntity = (billboard) => {
    if (!viewer || !billboard) return
    const material = new Cesium.ImageMaterialProperty({
        image: getPulseCanvas(),
        transparent: true,
    })
    const material = getPulseSphereMaterial()
    const pulse = {
        material,
        offset: Math.random() * PULSE_DURATION_S,
        entity: null,
    }
    const getPulseRadius3D = () => {
        const radius = getPulseRadius(pulse)
        return new Cesium.Cartesian3(radius, radius, radius)
    }
    pulse.entity = viewer.entities.add({
        position: new Cesium.CallbackProperty(() => billboard.position, false),
        ellipse: {
            semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
            semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
        position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false),
        ellipsoid: {
            radii: new Cesium.CallbackProperty(() => getPulseRadius3D(), false),
            material,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        },
    })
    favoritePulseEntities.push(pulse)
}
const getBillboardPosition = billboard => {
    if (!viewer || !billboard) return null
    const position = billboard.position
    if (position?.getValue) {
        return position.getValue(viewer.clock.currentTime)
    }
    return position
}
const getDevicePosition = item => {
@@ -411,6 +494,7 @@
const setDroneVisibility = visible => {
    if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
    if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
    if (droneTrackSource) droneTrackSource.show = visible
    if (favoritePulseEntities.length) {
        favoritePulseEntities.forEach(pulse => {
            if (pulse?.entity) pulse.entity.show = visible
@@ -537,6 +621,10 @@
        }
        droneTrackPolylineCollection = null
    }
    if (droneTrackSource) {
        viewer.dataSources.remove(droneTrackSource)
        droneTrackSource = null
    }
    droneTrackRuntime = []
    if (favoritePulseEntities.length) {
        favoritePulseEntities.forEach(pulse => {
@@ -570,44 +658,96 @@
    }
}
const ensureDroneTrackSource = () => {
    if (!viewer) return
    if (!droneTrackSource) {
        droneTrackSource = new Cesium.CustomDataSource('droneTrackSource')
        viewer.dataSources.add(droneTrackSource)
    }
}
const setupDroneTrackClock = () => {
    if (!viewer) return
    const startTime = Cesium.JulianDate.now()
    const stopTime = Cesium.JulianDate.addSeconds(startTime, DRONE_TRACK_DURATION_S, new Cesium.JulianDate())
    viewer.clock.startTime = startTime
    viewer.clock.stopTime = stopTime
    viewer.clock.currentTime = startTime
    viewer.clock.multiplier = 1
    viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER
    viewer.clock.clockRange = Cesium.ClockRange.CLAMPED
}
const updateDroneTrackPositions = elapsed => {
    if (!viewer || !viewer.scene || !droneTrackRuntime.length) return
    droneTrackRuntime.forEach(track => {
        if (!track?.billboard || track.billboard.isDestroyed?.()) return
        if (!track?.polyline) return
        if (!track.positions || track.positions.length < 2) return
        const duration = track.duration
        if (duration <= 0) return
        const t = Math.min(Math.max(elapsed, 0), duration)
        const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
        const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
        const isEntityPosition = typeof track.billboard?.position?.getValue === 'function'
        const pos = isEntityPosition
            ? getBillboardPosition(track.billboard)
            : Cesium.Cartesian3.lerp(
                    track.positions[seg],
                    track.positions[seg + 1],
                    ratio,
                    new Cesium.Cartesian3()
              )
        if (!pos) return
        if (!isEntityPosition) {
            track.billboard.position = pos
        }
        const pathCount = Math.min(track.positions.length - 1, Math.floor(t / track.segmentDuration))
        const pathPositions = track.positions.slice(0, pathCount + 1)
        track.polyline.positions = [...pathPositions, pos]
    })
}
const startDroneTrackAnimation = () => {
    if (!viewer) return
    stopDroneTrackAnimation()
    droneTrackStartTime = Cesium.JulianDate.now()
    droneTrackTickHandler = clock => {
        if (!viewer || viewer.isDestroyed?.() || !viewer.scene) {
            stopDroneTrackAnimation()
    droneTrackStartTime = viewer.clock.startTime
    viewer.clock.shouldAnimate = true
    droneTrackAnimStartAt = performance.now()
    droneTrackLastTickAt = droneTrackAnimStartAt
    const renderTick = now => {
        if (!viewer || viewer.isDestroyed?.()) {
            droneTrackRafId = null
            return
        }
        if (!droneTrackRuntime.length) return
        const current = clock.currentTime
        const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
        droneTrackRuntime.forEach(track => {
            if (!track?.billboard || track.billboard.isDestroyed?.()) return
            if (!track.billboard._scene) return
            if (!track.positions || track.positions.length < 2) return
            const duration = track.duration
            if (duration <= 0) return
            const t = ((elapsed % duration) + duration) % duration
            const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
            const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
            const pos = Cesium.Cartesian3.lerp(
                track.positions[seg],
                track.positions[seg + 1],
                ratio,
                new Cesium.Cartesian3()
        const deltaSeconds = Math.max(0, (now - droneTrackLastTickAt) / 1000)
        droneTrackLastTickAt = now
        if (viewer.clock?.currentTime && viewer.clock?.stopTime) {
            const nextTime = Cesium.JulianDate.addSeconds(
                viewer.clock.currentTime,
                deltaSeconds * (viewer.clock.multiplier || 1),
                new Cesium.JulianDate()
            )
            track.billboard.position = pos
        })
            if (Cesium.JulianDate.greaterThan(nextTime, viewer.clock.stopTime)) {
                viewer.clock.currentTime = viewer.clock.stopTime
            } else {
                viewer.clock.currentTime = nextTime
            }
        }
        const elapsed = Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, droneTrackStartTime)
        updateDroneTrackPositions(elapsed)
        viewer.scene?.requestRender?.()
        droneTrackRafId = requestAnimationFrame(renderTick)
    }
    viewer.clock.onTick.addEventListener(droneTrackTickHandler)
    viewer.clock.shouldAnimate = true
    droneTrackRafId = requestAnimationFrame(renderTick)
}
const stopDroneTrackAnimation = () => {
    if (!viewer || !droneTrackTickHandler) return
    viewer.clock.onTick.removeEventListener(droneTrackTickHandler)
    droneTrackTickHandler = null
    if (droneTrackRafId) {
        cancelAnimationFrame(droneTrackRafId)
        droneTrackRafId = null
    }
}
const startFavoritePulseAnimation = () => {
@@ -637,10 +777,11 @@
    clearDroneTrackEntities()
    if (!list?.length) return
    ensureDroneTrackCollections()
    ensureDroneTrackSource()
    setupDroneTrackClock()
    droneTrackBillboardCollection.show = detailVisible.value
    droneTrackPolylineCollection.show = detailVisible.value
    droneTrackRuntime = []
    const segmentDuration = 6
    const baseTrackColor = Cesium.Color.fromCssColorString('red')
        ; (list || []).forEach((item, trackIndex) => {
            const position = getDevicePosition(item)
@@ -650,6 +791,7 @@
            const positions = points.map(point =>
                Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
            )
            const segmentDuration = DRONE_TRACK_DURATION_S / Math.max(positions.length - 1, 1)
            let trackMaterial = createDroneTrackMaterial({
                color: baseTrackColor,
                speed: 4.5,
@@ -660,43 +802,67 @@
            if (!trackMaterial) {
                trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
            }
            droneTrackPolylineCollection.add({
                positions,
            const polyline = droneTrackPolylineCollection.add({
                positions: [positions[0]],
                width: 3,
                material: trackMaterial,
            })
            const droneId = `drone-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
            const billboard = droneTrackBillboardCollection.add({
                position: positions[positions.length - 1],
                image: droneIcon,
                width: 36,
                height: 36,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            const startTime = viewer.clock.startTime
            const stopTime = viewer.clock.stopTime
            const positionProperty = new Cesium.SampledPositionProperty()
            positions.forEach((pos, index) => {
                const sampleTime = Cesium.JulianDate.addSeconds(
                    startTime,
                    index * segmentDuration,
                    new Cesium.JulianDate()
                )
                positionProperty.addSample(sampleTime, pos)
            })
            billboard.id = droneId
            positionProperty.setInterpolationOptions({
                interpolationDegree: 1,
                interpolationAlgorithm: Cesium.LinearApproximation,
            })
            const entity = droneTrackSource.entities.add({
                id: droneId,
                availability: new Cesium.TimeIntervalCollection([
                    new Cesium.TimeInterval({
                        start: startTime,
                        stop: stopTime,
                    }),
                ]),
                position: positionProperty,
                orientation: new Cesium.VelocityOrientationProperty(positionProperty),
                billboard: {
                    image: droneIcon,
                    width: 36,
                    height: 36,
                    verticalOrigin: Cesium.VerticalOrigin.CENTER,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
            })
            const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
        dronePickMap.set(droneId, {
            data: {
                ...item,
                flightHeightM: item.flightHeightM ?? points[0].height,
                flightSpeedMs: item.flightSpeedMs ?? speedMs,
                longitude: item.longitude ?? points[0].longitude,
                latitude: item.latitude ?? points[0].latitude,
            },
            billboard,
        })
        if (isFavorited(item)) {
            createFavoritePulseEntity(billboard)
        }
            dronePickMap.set(droneId, {
                data: {
                    ...item,
                    flightHeightM: item.flightHeightM ?? points[0].height,
                    flightSpeedMs: item.flightSpeedMs ?? speedMs,
                    longitude: item.longitude ?? points[0].longitude,
                    latitude: item.latitude ?? points[0].latitude,
                },
                billboard: entity,
            })
            if (isFavorited(item)) {
                createFavoritePulseEntity(entity)
            }
            droneTrackRuntime.push({
                positions,
                billboard,
                polyline,
                billboard: entity,
                segmentDuration,
                duration: (points.length - 1) * segmentDuration,
                duration: DRONE_TRACK_DURATION_S,
            })
        })
    viewer.clock.multiplier = 1
    startDroneTrackAnimation()
    startFavoritePulseAnimation()
    reorderCockpitPrimitives()
@@ -1045,7 +1211,7 @@
const updatePopupPosition = () => {
    if (!viewer || !selectedDeviceBillboard) return
    const cartesian = selectedDeviceBillboard.position
    const cartesian = getBillboardPosition(selectedDeviceBillboard)
    if (!cartesian) return
    const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
    if (!screenPosition) return
@@ -1129,7 +1295,7 @@
const loadAggregation = async () => {
    try {
        const res = await cockpitAggregationApi({
        const res = await newCockpitAggregationApi({
            effectiveRangeKmIsNotNull: 1
        })
        renderAggregation(res?.data?.data ?? [])
applications/drone-command/src/utils/cesium/DrawPolygon.js
File was deleted
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js
@@ -1,7 +1,12 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '@ztzf/utils'
import { ToolBase } from '../ToolBase'
import { buildEllipsePositions } from '../shapeUtils'
import {
    buildEllipsePositions,
    formatBufferRadius,
    getBufferRadiusLabelStyle,
    getBufferRadiusPoint,
} from '../shapeUtils'
export class DrawBufferCircleTool extends ToolBase {
    constructor(viewer) {
@@ -21,6 +26,9 @@
        this.radiusPoint1 = null
        this.radiusPoint2 = null
        this.radiusPoint3 = null
        this.radiusLevel1LabelEntity = null
        this.radiusLevel2LabelEntity = null
        this.radiusLevel3LabelEntity = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
@@ -193,6 +201,10 @@
                material: Cesium.Color.fromBytes(0, 251, 255, 255),
            },
        })
        this.radiusLevel1LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel1)
        this.radiusLevel2LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel2)
        this.radiusLevel3LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel3)
    }
    clearPreviewEntities() {
@@ -204,6 +216,9 @@
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.radiusLevel1LabelEntity = null
        this.radiusLevel2LabelEntity = null
        this.radiusLevel3LabelEntity = null
    }
    getSurfaceDistance(startCartesian, endCartesian) {
@@ -218,6 +233,23 @@
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    getRadiusPoint(radius) {
        return getBufferRadiusPoint(this.centerCartesian, radius)
    }
    createRadiusLabelEntity(getRadius) {
        return this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(
                () => getBufferRadiusPoint(this.centerCartesian, getRadius()),
                false
            ),
            label: {
                ...getBufferRadiusLabelStyle(),
                text: new Cesium.CallbackProperty(() => formatBufferRadius(getRadius()), false),
            },
        })
    }
    destroy() {
@@ -241,6 +273,9 @@
        this.radiusPoint1 = null
        this.radiusPoint2 = null
        this.radiusPoint3 = null
        this.radiusLevel1LabelEntity = null
        this.radiusLevel2LabelEntity = null
        this.radiusLevel3LabelEntity = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js
@@ -1,7 +1,13 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '@ztzf/utils'
import { ToolBase } from '../ToolBase'
import { buildEllipsePositions, cartesianToLocal } from '../shapeUtils'
import {
    buildEllipsePositions,
    cartesianToLocal,
    formatBufferRadius,
    getBufferRadiusLabelStyle,
    getBufferRadiusPoint,
} from '../shapeUtils'
const normalizePoints = points =>
    (points || []).map(point => ({
@@ -49,6 +55,9 @@
        this.radiusLevel1Entity = null
        this.radiusLevel2Entity = null
        this.radiusLevel3Entity = null
        this.radiusLevel1LabelEntity = null
        this.radiusLevel2LabelEntity = null
        this.radiusLevel3LabelEntity = null
    }
    start(data = []) {
@@ -272,6 +281,10 @@
            },
            customType: 'circle-radius-3',
        })
        this.radiusLevel1LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel1)
        this.radiusLevel2LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel2)
        this.radiusLevel3LabelEntity = this.createRadiusLabelEntity(() => this.radiusLevel3)
    }
    updateRadiusPoints() {
@@ -298,9 +311,20 @@
    }
    getRadiusPoint(radius) {
        const frame = Cesium.Transforms.eastNorthUpToFixedFrame(this.centerCartesian)
        const local = new Cesium.Cartesian3(radius, 0, 0)
        return Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3())
        return getBufferRadiusPoint(this.centerCartesian, radius)
    }
    createRadiusLabelEntity(getRadius) {
        return this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(
                () => getBufferRadiusPoint(this.centerCartesian, getRadius()),
                false
            ),
            label: {
                ...getBufferRadiusLabelStyle(),
                text: new Cesium.CallbackProperty(() => formatBufferRadius(getRadius()), false),
            },
        })
    }
    getPositionFromScreen(screenPosition) {
@@ -346,6 +370,9 @@
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.radiusLevel1LabelEntity = null
        this.radiusLevel2LabelEntity = null
        this.radiusLevel3LabelEntity = null
        this.dragType = null
    }
}
applications/drone-command/src/utils/cesium/shapeTools/index.js
@@ -8,6 +8,10 @@
    getCenterFromBounds,
    getMetersBetween,
    cartesianToLocal,
    formatBufferRadius,
    getBufferRadiusPoint,
    getBufferRadiusLabelStyle,
    addBufferRadiusLabelEntity,
} from './shapeUtils'
export { DrawPolygonTool } from './draw/DrawPolygonTool'
export { DrawRectangleTool } from './draw/DrawRectangleTool'
applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js
@@ -87,3 +87,47 @@
    const inverse = Cesium.Matrix4.inverse(frame, new Cesium.Matrix4())
    return Cesium.Matrix4.multiplyByPoint(inverse, positionCartesian, new Cesium.Cartesian3())
}
export const formatBufferRadius = meters => {
    if (!Number.isFinite(meters)) return ''
    if (meters >= 1000) return `${(meters / 1000).toFixed(2)} km`
    return `${meters.toFixed(2)} m`
}
export const getBufferRadiusPoint = (centerCartesian, radius) => {
    if (!centerCartesian || !Number.isFinite(radius)) return null
    const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian)
    const local = new Cesium.Cartesian3(radius, 0, 0)
    return Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3())
}
export const getBufferRadiusLabelStyle = () => ({
    font: '14px Source Han Sans CN',
    fillColor: Cesium.Color.WHITE,
    outlineColor: Cesium.Color.fromBytes(24, 48, 92, 255),
    outlineWidth: 2,
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    showBackground: true,
    backgroundColor: Cesium.Color.fromBytes(8, 12, 22, 180),
    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
    pixelOffset: new Cesium.Cartesian2(0, -12),
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
})
export const addBufferRadiusLabelEntity = (dataSource, centerCartesian, radius, options = {}) => {
    if (!dataSource) return null
    const position = getBufferRadiusPoint(centerCartesian, radius)
    if (!position) return null
    const { name, customData } = options
    return dataSource.entities.add({
        ...(name ? { name } : {}),
        ...(customData ? { customData } : {}),
        position,
        label: {
            ...getBufferRadiusLabelStyle(),
            text: formatBufferRadius(radius),
        },
    })
}
applications/drone-command/src/views/api.txt
@@ -1,22 +1,34 @@
## 列表
## 根据多边形区域查询派出所
**接口地址**:`/area/fwDefenseSceneManage/list`
**接口地址**:`/area/fwPoliceStation/queryByPolygons`
**请求方式**:`GET`
**请求方式**:`POST`
**请求数据类型**:`application/x-www-form-urlencoded`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入time</p>
**接口描述**:<p>传入多边形WKT字符串集合</p>
**请求示例**:
```javascript
{
  "polygons": [
    "POLYGON((114.96898636473107 27.11156483383477,114.97972069576815 27.111564833834766,114.97972069576815 27.120473921808035,114.96898636473107 27.120473921808035,114.96898636473107 27.11156483383477))"
  ]
}
```
**请求参数**:
@@ -27,7 +39,8 @@
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|time|时间筛选(yyyy-MM-dd HH:mm:ss)|query|false|string(date-time)||
|dto|派出所多边形区域查询入参|body|true|FwPoliceStationPolygonQueryDTO对象|FwPoliceStationPolygonQueryDTO对象|
|&emsp;&emsp;polygons|多边形WKT字符串集合||true|array|string|
**响应状态**:
@@ -35,7 +48,8 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- | 
|200|OK|R«List«FwDefenseSceneManageVO»»|
|200|OK|R«List«FwPoliceStationVO»»|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -47,18 +61,21 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- | 
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|array|FwDefenseSceneManageVO|
|data|承载数据|array|FwPoliceStationVO|
|&emsp;&emsp;address|派出所地址|string||
|&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;defenseLeader|防控负责人|string||
|&emsp;&emsp;defenseSceneId|关联场景配置ID|integer(int64)||
|&emsp;&emsp;defenseSceneName|场景配置名称|string||
|&emsp;&emsp;effectiveDateEnd|有效时间-结束|string(date-time)||
|&emsp;&emsp;effectiveDateStart|有效时间-开始|string(date-time)||
|&emsp;&emsp;id||integer(int64)||
|&emsp;&emsp;latitude|指挥点纬度|number(double)||
|&emsp;&emsp;leaderPhone|防控负责人电话|string||
|&emsp;&emsp;longitude|指挥点经度|number(double)||
|&emsp;&emsp;sceneName|场景名称|string||
|&emsp;&emsp;contactPerson|联系人|string||
|&emsp;&emsp;contactPhone|联系方式|string||
|&emsp;&emsp;createDept|创建部门|integer(int64)||
|&emsp;&emsp;createTime|创建时间|string(date-time)||
|&emsp;&emsp;createUser|创建人|integer(int64)||
|&emsp;&emsp;id|主键id|integer(int64)||
|&emsp;&emsp;latitude|纬度|number(double)||
|&emsp;&emsp;longitude|经度|number(double)||
|&emsp;&emsp;stationName|派出所名称|string||
|&emsp;&emsp;status|状态(0正常 1停用)|integer(int32)||
|&emsp;&emsp;updateTime|更新时间|string(date-time)||
|&emsp;&emsp;updateUser|更新人|integer(int64)||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -69,17 +86,20 @@
    "code": 0,
    "data": [
        {
            "address": "",
            "areaCode": "",
            "defenseLeader": "",
            "defenseSceneId": 0,
            "defenseSceneName": "",
            "effectiveDateEnd": "",
            "effectiveDateStart": "",
            "contactPerson": "",
            "contactPhone": "",
            "createDept": 0,
            "createTime": "",
            "createUser": 0,
            "id": 0,
            "latitude": 0,
            "leaderPhone": "",
            "longitude": 0,
            "sceneName": ""
            "stationName": "",
            "status": 0,
            "updateTime": "",
            "updateUser": 0
        }
    ],
    "msg": "",
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue
@@ -16,7 +16,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>
@@ -105,7 +105,7 @@
import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import { DrawPolygon } from '@ztzf/utils'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import * as turf from '@turf/turf'
import * as Cesium from 'cesium'
applications/drone-command/src/views/areaManage/partition/1.json
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
@@ -17,7 +17,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>
@@ -180,7 +180,7 @@
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 { DrawPolygon } from '@ztzf/utils'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import * as turf from '@turf/turf'
import * as Cesium from 'cesium'
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -3,7 +3,7 @@
        <div class="dialog-container">
            <div class="left-container">
                <CommonCesiumMap ref="mapRef" class="leftMap command-cesium" :active="visible" :flat-mode="false"
                    :terrain="true" :layer-mode="4" :boundary="false" />
                    :terrain="true" :layer-mode="4" :boundary="false" :zoom-to-boundary="dialogMode === 'add'" />
                <div v-if="showTypePanel && !readonly" class="shape-type-panel">
                    <div class="panel-header">
                        <span>{{ drawTypeLabelMap[currentShapeType] || '区域' }}</span>
@@ -14,8 +14,8 @@
                    <div class="panel-body">
                        <span class="panel-label">区域类型</span>
                        <el-select class="command-select" popper-class="command-select-popper" v-model="activeAreaType"
                            placeholder="请选择" :disabled="currentShapeType === 'buffer'" collapse-tags :multiple="currentShapeType === 'buffer'"
                            @change="handleAreaTypeChange">
                            placeholder="请选择" :disabled="currentShapeType === 'buffer'" collapse-tags
                            :multiple="currentShapeType === 'buffer'" @change="handleAreaTypeChange">
                            <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
@@ -226,14 +226,19 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi } from './partitionApi'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { DrawManager, EditManager, buildEllipsePositions } from '@/utils/cesium/shapeTools'
import {
    DrawManager,
    EditManager,
    addBufferRadiusLabelEntity,
    buildEllipsePositions,
} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import {
    createDeviceRangePrimitive,
@@ -243,8 +248,8 @@
} 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 { fwPoliceStationQueryByPolygonsApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListByPolygonsApi } from '@/views/basicManage/deviceStock/fwDevice'
import polygonIcon from '@/assets/images/areaMap/polygon.png'
import rectIcon from '@/assets/images/areaMap/rect.png'
import ellipseIcon from '@/assets/images/areaMap/ellipse.png'
@@ -302,8 +307,13 @@
const deviceRangePrimitiveMap = new Map()
const activeToolMode = ref('')
const suppressDeviceFly = ref(false)
const isDialogInitializing = ref(false)
let lastSelectedDeviceIds = []
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
let policeStationListTimer = null
let policeStationRequestToken = 0
const rules = {
    areaName: fieldRules(true, 50),
@@ -406,6 +416,14 @@
    () => visible.value,
    val => {
        if (!val) {
            if (deviceListTimer) {
                clearTimeout(deviceListTimer)
                deviceListTimer = null
            }
            if (policeStationListTimer) {
                clearTimeout(policeStationListTimer)
                policeStationListTimer = null
            }
            viewEntity && viewer?.entities?.remove(viewEntity)
            viewEntity = null
            clearActiveTool()
@@ -420,6 +438,15 @@
    val => {
        updateActiveShapeAreaType(val)
    }
)
watch(
    () => shapeList.value,
    () => {
        if (isDialogInitializing.value) return
        queueDeviceListRefresh()
        queuePoliceStationRefresh()
    },
    { deep: true }
)
// 加载详情
@@ -469,6 +496,103 @@
        viewer.scene.primitives.remove(primitive)
    }
    deviceRangePrimitiveMap.clear()
}
function clearDeviceListState () {
    deviceOptions.value = []
    selectedDeviceRows.value = []
    formData.value.deviceIds = ''
    if (deviceTableRef.value) {
        deviceTableRef.value.clearSelection()
    }
}
function clearPoliceStationState () {
    policeStationOptions.value = []
    formData.value.policeStationId = ''
}
function formatWktNumber (value) {
    if (!Number.isFinite(Number(value))) return null
    return Number(value).toFixed(6)
}
function isSameWktPoint (left, right) {
    if (!left || !right) return false
    return Math.abs(left.lat - right.lat) < 0.000001 && Math.abs(left.lng - right.lng) < 0.000001
}
function buildPolygonWktFromPoints (points) {
    if (!Array.isArray(points) || points.length < 3) return null
    const ring = points
        .map(resolveLngLatPoint)
        .filter(Boolean)
        .map(point => ({ lng: point.lng, lat: point.lat }))
    if (ring.length < 3) return null
    const first = ring[0]
    const last = ring[ring.length - 1]
    if (!isSameWktPoint(first, last)) {
        ring.push({ ...first })
    }
    const coordText = ring
        .map(point => {
            const lng = formatWktNumber(point.lng)
            const lat = formatWktNumber(point.lat)
            if (lat === null || lng === null) return null
            return `${lng} ${lat}`
        })
        .filter(Boolean)
        .join(', ')
    if (!coordText) return null
    return `POLYGON((${coordText}))`
}
function buildEllipseWktFromMeta (center, semiMajor, semiMinor) {
    if (!viewer) return null
    const resolvedCenter = resolveLngLatPoint(center)
    if (!resolvedCenter) return null
    if (!Number.isFinite(Number(semiMajor)) || !Number.isFinite(Number(semiMinor))) return null
    const centerCartesian = Cesium.Cartesian3.fromDegrees(
        resolvedCenter.lng,
        resolvedCenter.lat,
        resolvedCenter.height || 0
    )
    const positions = buildEllipsePositions(centerCartesian, Number(semiMajor), Number(semiMinor))
    if (!positions.length) return null
    const points = positions
        .map(item => {
            const converted = cartesian3Convert(item, viewer)
            return { lng: converted.longitude, lat: converted.latitude }
        })
        .filter(Boolean)
    return buildPolygonWktFromPoints(points)
}
function resolveShapePolygonsWkt () {
    const polygons = []
    shapeList.value.forEach(shape => {
        if (!shape) return
        if (shape.drawType === 'buffer') {
            const radii = Array.isArray(shape?.meta?.bufferRadii)
                ? shape.meta.bufferRadii.filter(item => Number.isFinite(item) && item > 0)
                : []
            const maxRadius = radii.length ? Math.max(...radii) : null
            const wkt = buildEllipseWktFromMeta(shape?.meta?.center, maxRadius, maxRadius)
            if (wkt) polygons.push(wkt)
            return
        }
        if (shape.drawType === 'ellipse') {
            const wkt = buildEllipseWktFromMeta(shape?.meta?.center, shape?.meta?.semiMajor, shape?.meta?.semiMinor)
            if (wkt) {
                polygons.push(wkt)
                return
            }
        }
        const points = getShapeDisplayPoints(shape)
        const wkt = buildPolygonWktFromPoints(points)
        if (wkt) polygons.push(wkt)
    })
    return polygons
}
async function renderDeviceRangeSingle (device) {
@@ -649,6 +773,10 @@
                            width: 2,
                            material: style.outline,
                        },
                    })
                    addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
                        name: 'shape-display',
                        customData: { drawType: 'buffer', label: 'radius' },
                    })
                })
            }
@@ -1060,18 +1188,77 @@
}
// 获取派出所列表
async function getPoliceStationList () {
    const res = await fwPoliceStationListApi()
function queuePoliceStationRefresh () {
    if (!visible.value) return
    if (policeStationListTimer) {
        clearTimeout(policeStationListTimer)
    }
    policeStationListTimer = setTimeout(() => {
        policeStationListTimer = null
        void getPoliceStationListByPolygons()
    }, 200)
}
async function getPoliceStationListByPolygons () {
    if (!visible.value) return
    const polygons = resolveShapePolygonsWkt()
    if (!polygons.length) {
        clearPoliceStationState()
        return
    }
    policeStationRequestToken += 1
    const requestToken = policeStationRequestToken
    const res = await fwPoliceStationQueryByPolygonsApi({ polygons })
    if (requestToken !== policeStationRequestToken) return
    policeStationOptions.value = res?.data?.data ?? []
    await nextTick()
    const matched = policeStationOptions.value.find(
        item => String(item.id) === String(formData.value.policeStationId)
    )
    if (!matched) {
        formData.value.policeStationId = ''
    }
}
// 获取设备列表
async function getDeviceList () {
    if (deviceOptions.value.length) return
    const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id, isTrack: 1 })
function queueDeviceListRefresh () {
    if (!visible.value) return
    if (deviceListTimer) {
        clearTimeout(deviceListTimer)
    }
    deviceListTimer = setTimeout(() => {
        deviceListTimer = null
        void getDeviceListByPolygons()
    }, 200)
}
async function getDeviceListByPolygons () {
    if (!visible.value) return
    const polygons = resolveShapePolygonsWkt()
    if (!polygons.length) {
        clearDeviceListState()
        return
    }
    deviceListRequestToken += 1
    const requestToken = deviceListRequestToken
    const res = await fwDeviceListByPolygonsApi({
        isAreaSelect: 1,
        isTrack: 1,
        polygons,
    })
    if (requestToken !== deviceListRequestToken) return
    deviceOptions.value = res?.data?.data ?? []
    if (dialogMode.value === 'view') {
        deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id))
    if (dialogMode.value === 'view' && formData.value.deviceIds) {
        const targetIds = formData.value.deviceIds.split(',')
        deviceOptions.value = deviceOptions.value.filter(item => targetIds.includes(String(item.id)))
    }
    await nextTick()
    const prevSuppressDeviceFly = suppressDeviceFly.value
    suppressDeviceFly.value = true
    syncDeviceSelection()
    suppressDeviceFly.value = prevSuppressDeviceFly
    if (!readonly.value) {
        renderDeviceRanges(selectedDeviceRows.value)
    }
}
@@ -1105,14 +1292,16 @@
// 同步选择状态
function syncDeviceSelection () {
    if (isDialogInitializing.value && !deviceOptions.value.length) return
    const rows = []
    const arr = formData.value.deviceIds.split(',')
    const arr = formData.value.deviceIds ? formData.value.deviceIds.split(',') : []
    deviceOptions.value.forEach(row => {
        if (arr.includes(row.id)) {
        if (arr.includes(String(row.id))) {
            rows.push(row)
        }
    })
    selectedDeviceRows.value = rows
    formData.value.deviceIds = rows.map(item => item.id).join(',')
    if (readonly.value) {
        renderDeviceRanges(selectedDeviceRows.value)
        return
@@ -1126,9 +1315,12 @@
// 打开弹框
async function open ({ mode, row } = {}) {
    isDialogInitializing.value = true
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedDeviceRows.value = []
    deviceOptions.value = []
    policeStationOptions.value = []
    shapeList.value = []
    activeShapeId.value = null
    activeAreaType.value = ''
@@ -1140,8 +1332,8 @@
    viewEntity && viewer?.entities?.remove(viewEntity)
    viewEntity = null
    clearActiveTool()
    await getDeviceList()
    if (dialogMode.value === 'add') {
        mapRef.value?.zoomToAdminBoundary?.()
        // default no draw mode
    } else if (dialogMode.value === 'edit') {
        await loadDetail()
@@ -1159,13 +1351,20 @@
    if (!readonly.value) {
        renderDeviceRanges(selectedDeviceRows.value)
    }
    isDialogInitializing.value = false
    queueDeviceListRefresh()
    queuePoliceStationRefresh()
}
onMounted(() => {
    getPoliceStationList()
})
onBeforeUnmount(() => {
    if (deviceListTimer) {
        clearTimeout(deviceListTimer)
        deviceListTimer = null
    }
    if (policeStationListTimer) {
        clearTimeout(policeStationListTimer)
        policeStationListTimer = null
    }
    hideTypePanel()
})
applications/drone-command/src/views/areaManage/precinctInfo/precinctInfoApi.js
@@ -18,6 +18,15 @@
    })
}
// 多边形区域查询派出所
export const fwPoliceStationQueryByPolygonsApi = data => {
    return request({
        url: `/drone-fw/area/fwPoliceStation/queryByPolygons`,
        method: 'post',
        data,
    })
}
// 增加或更新
export const fwPoliceStationSubmitApi = data => {
    return request({
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -160,7 +160,10 @@
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import {
    addBufferRadiusLabelEntity,
    buildEllipsePositions,
} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
@@ -412,6 +415,10 @@
                            material: style.outline,
                        },
                    })
                    addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
                        name: 'scene-area-display',
                        customData: { drawType: 'buffer', label: 'radius' },
                    })
                })
                return
            }
@@ -575,8 +582,6 @@
    initMap()
    await getAreaList()
    if (dialogMode.value === 'add') {
        console.log(111)
        mapRef.value?.zoomToAdminBoundary?.()
    } else {
        await loadDetail()
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue
@@ -152,7 +152,10 @@
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import {
    addBufferRadiusLabelEntity,
    buildEllipsePositions,
} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { MapTooltip } from '@ztzf/utils'
@@ -464,6 +467,10 @@
                            material: style.outline,
                        },
                    })
                    addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
                        name: 'scene-manage-area-display',
                        customData: { drawType: 'buffer', label: 'radius' },
                    })
                })
                return
            }
applications/drone-command/src/views/basicManage/deviceStock/fwDevice.js
@@ -18,6 +18,15 @@
    })
}
// 多边形过滤设备列表
export const fwDeviceListByPolygonsApi = data => {
    return request({
        url: `/drone-fw/device/fwDevice/listByPolygons`,
        method: 'post',
        data,
    })
}
// 增加或更新
export const fwDeviceSubmitApi = data => {
    return request({
applications/drone-command/src/views/layerManagement/components/folderFile.vue
File was deleted
applications/drone-command/src/views/layerManagement/components/leftList.vue
File was deleted
applications/drone-command/src/views/layerManagement/components/nationalSpatialPlanning.vue
File was deleted
applications/drone-command/src/views/layerManagement/components/rightEdit.vue
File was deleted
applications/drone-command/src/views/layerManagement/components/utils.js
File was deleted
applications/drone-command/src/views/layerManagement/index.vue
File was deleted
applications/task-work-order/env/.env
@@ -21,3 +21,5 @@
# 预览地址 previewURL
VITE_APP_PREVIEW_URL=http://192.168.1.204:8012
VITE_APP_TERRAIN_URL=https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ztzf_c_uas/
applications/task-work-order/env/.env.development
@@ -31,8 +31,6 @@
# 航线文件地址
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/minio/cloud-bucket
# 图片存放地址
VITE_APP_TERRAIN_URL = https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ztzf_terrain/
# 行政区划存放地址
VITE_APP_REGION_URL = https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ztzf_region
# 算法仓库图片地址
applications/task-work-order/src/components/PlaybackVideo/PlaybackVideo.vue
File was deleted
applications/task-work-order/src/components/PlaybackVideo/components/MapContainer.vue
File was deleted
applications/task-work-order/src/components/PlaybackVideo/components/PhotoList.vue
File was deleted
applications/task-work-order/src/components/PlaybackVideo/components/VideoPlayer.vue
File was deleted
applications/task-work-order/src/components/map-container/mapContainer.vue
File was deleted
applications/task-work-order/src/main.js
@@ -50,7 +50,6 @@
// 业务组件
import tenantPackage from './views/system/tenantpackage.vue'
// 地图依赖
import mapContainer from './components/map-container/mapContainer.vue'
import * as DC from '@dvgis/dc-sdk'
import '@dvgis/dc-sdk/dist/dc.min.css'
@@ -102,7 +101,6 @@
app.component('thirdRegister', thirdRegister)
app.component('flowDesign', flowDesign)
app.component('tenantPackage', tenantPackage)
app.component('mapContainer', mapContainer)
app.config.globalProperties.$dayjs = dayjs
app.config.globalProperties.website = website
applications/task-work-order/src/router/views/index.js
@@ -134,29 +134,4 @@
      },
    ],
  },
  {
    path: '/resource',
    component: Layout,
    redirect: '/resource/patchManagement',
    children: [
      {
        path: 'patchManagement',
        name: '图斑管理',
        meta: {
          i18n: 'info',
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/resource/patchManagement.vue'),
      },
      {
        path: 'patchTypeManagement',
        name: '图斑类型管理',
        meta: {
          i18n: 'info',
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/resource/patchTypeManagement.vue'),
      },
    ],
  },
]
applications/task-work-order/src/utils/cesium/publicCesium.js
@@ -146,52 +146,7 @@
        this.viewer?.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 禁用双击
        if (terrain) {
            try {
                const noTerrainMechanism = ['1962779164650135554']
                if (noTerrainMechanism.includes(store.state.user.userInfo?.deptId)) {
                    // 使用Cesium World Terrain
                    Cesium.createWorldTerrainAsync({
                        requestWaterMask: false,    // 请求水域效果
                        requestVertexNormals: true // 请求光照和地形法线
                    }).then(terrainProvider => {
                        this.viewer.terrainProvider = terrainProvider
                        terrainLoadCallback?.()
                    })
                } else {
                    /*// 正则:第一位非0,第二位任意数字,后10位都是0
                    const re = /^[1-9]\d0{10}$/
                    let result = null
                    // 1️⃣先判断 areaCode
                    if (re.test(store.state.user.userInfo.detail.areaCode)) {
                        result = store.state.user.userInfo.detail.areaCode.slice(0, 6)
                    } else {
                        // 2️⃣如果不满足,从 ancestors 里找
                        const matchedItems = store.state.user.userInfo.detail.ancestors
                            .split(',')
                            .filter(item => re.test(item)) // 过滤出符合条件的
                        // 取第一个符合条件的前6位(可以改成 last one 看需求)
                        if (matchedItems.length > 0) {
                            result = matchedItems[0].slice(0, 6)
                        }
                    }
                    // 使用公司地形
                    Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, {
                        requestVertexNormals: true, // 启用地形法线增强立体感
                        requestWaterMask: true, // 启用水体遮罩效果
                    }).then(terrainProvider => {
                        this.viewer.terrainProvider = terrainProvider
                        terrainLoadCallback?.()
                    })*/
                }
            } catch (error) {
                console.error('地形加载失败:', error)
            }
        }
        this.setTerrainVisible(terrain, terrainLoadCallback)
        this.viewer.scene.screenSpaceCameraController.maximumZoomDistance = 4500000
        this.switchLayers(layerMode)
@@ -205,6 +160,39 @@
            this.viewer.resolutionScale = dpr // 设置分辨率缩放比例
        }
    }
    async ensureTerrainProvider () {
        if (this.terrainProvider) return this.terrainProvider
        if (this.terrainLoading) return this.terrainLoading
        this.terrainLoading = Cesium.CesiumTerrainProvider.fromUrl(
            `${import.meta.env.VITE_APP_TERRAIN_URL}ja_terrain`,
            {
                requestVertexNormals: true, // 启用地形法线增强立体感
                requestWaterMask: true, // 启用水体遮罩效果
            }
        ).then(provider => {
            this.terrainProvider = provider
            return provider
        })
        return this.terrainLoading
    }
    async setTerrainVisible (visible, terrainLoadCallback) {
        if (!this.viewer) return
        if (!visible) {
            this.viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider()
            this.terrainEnabled = false
            return
        }
        try {
            const provider = await this.ensureTerrainProvider()
            if (!this.viewer) return
            this.viewer.terrainProvider = provider
            this.terrainEnabled = true
            terrainLoadCallback?.()
        } catch (error) {
            this.terrainEnabled = false
        }
    }
    getViewer () {
        return this.viewer
applications/task-work-order/src/views/orderView/appConfiguration/inventory/index.vue
New file
@@ -0,0 +1,20 @@
<template>
 <div>
    实施清单
 </div>
</template>
<script setup >
/**
 * @description index
 * @date 2026-02-02 (周一) 09:25:52
 */
defineOptions({
 name: 'index'
})
</script>
<style scoped >
</style>
applications/task-work-order/src/views/orderView/appConfiguration/inventory/接口文档.txt
New file
@@ -0,0 +1,105 @@
## 分页
**接口地址**:`/implement/gdImplementList/page`
**请求方式**:`GET`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>传入gdImplementList</p>
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|areaCode|所属区划编码|query|false|string||
|current|当前页|query|false|integer(int32)||
|matterCode|事项编码|query|false|string||
|matterName|事项名称|query|false|string||
|size|每页的数量|query|false|integer(int32)||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R«IPage«GdImplementListVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|IPage«GdImplementListVO»|IPage«GdImplementListVO»|
|&emsp;&emsp;current||integer(int64)||
|&emsp;&emsp;pages||integer(int64)||
|&emsp;&emsp;records||array|GdImplementListVO|
|&emsp;&emsp;&emsp;&emsp;areaCode|所属区划编码|string||
|&emsp;&emsp;&emsp;&emsp;areaName|所属区划名称|string||
|&emsp;&emsp;&emsp;&emsp;catalogCode|目录编码|string||
|&emsp;&emsp;&emsp;&emsp;id|主键id|integer||
|&emsp;&emsp;&emsp;&emsp;implementCode|实施编码|string||
|&emsp;&emsp;&emsp;&emsp;implementName|实施清单名称|string||
|&emsp;&emsp;&emsp;&emsp;matterCode|事项编码|string||
|&emsp;&emsp;&emsp;&emsp;matterName|事项名称|string||
|&emsp;&emsp;&emsp;&emsp;orgCode|所属机构编码|string||
|&emsp;&emsp;&emsp;&emsp;orgName|所属机构名称|string||
|&emsp;&emsp;&emsp;&emsp;processDefinitionId|流程定义主键|string||
|&emsp;&emsp;&emsp;&emsp;processInstanceId|流程实例主键|string||
|&emsp;&emsp;size||integer(int64)||
|&emsp;&emsp;total||integer(int64)||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {
        "current": 0,
        "pages": 0,
        "records": [
            {
                "areaCode": "",
                "areaName": "",
                "catalogCode": "",
                "id": 0,
                "implementCode": "",
                "implementName": "",
                "matterCode": "",
                "matterName": "",
                "orgCode": "",
                "orgName": "",
                "processDefinitionId": "",
                "processInstanceId": ""
            }
        ],
        "size": 0,
        "total": 0
    },
    "msg": "",
    "success": true
}
```
applications/task-work-order/src/views/orderView/appConfiguration/materials/FormDiaLog.vue
New file
@@ -0,0 +1,303 @@
<template>
    <el-dialog
        class="gd-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        @closed="visible = false"
        destroy-on-close
        :close-on-click-modal="false"
    >
        <el-row class="detail-row-view" v-if="dialogReadonly">
            <el-col :span="12">
                <div class="label">材料名称</div>
                <div class="val">{{ formData.materialName }}</div>
            </el-col>
            <el-col :span="12">
                <div class="label">材料说明</div>
                <div class="val">{{ formData.materialDesc }}</div>
            </el-col>
            <el-col :span="12">
                <div class="label">必要性</div>
                <div class="val">{{ getDictLabel(formData.necessity, dictObj.necessity) }}</div>
            </el-col>
            <el-col :span="12">
                <div class="label">是否适用全部情形</div>
                <div class="val">{{ getDictLabel(String(formData.isAllScenario), dictObj.isAllScenario) }}</div>
            </el-col>
            <!-- <el-col :span="12">
                <div class="label">所属区域</div>
                <div class="val">{{ formData.areaName }}</div>
            </el-col> -->
        </el-row>
        <el-form
            class="gd-dialog-form"
            v-else
            ref="formRef"
            :model="formData"
            :rules="rules"
            :disabled="dialogReadonly"
            label-width="140px"
        >
            <el-row>
                <el-col :span="12">
                    <el-form-item label="材料名称" prop="materialName">
                        <el-input class="gd-input" v-model="formData.materialName" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="材料说明" prop="materialDesc">
                        <el-input class="gd-input" v-model="formData.materialDesc" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="必要性" prop="necessity">
                        <el-select
                            class="gd-select"
                            popper-class="gd-select-popper"
                            v-model="formData.necessity"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.necessity"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="是否适用全部情形" prop="isAllScenario">
                        <el-select
                            class="gd-select"
                            popper-class="gd-select-popper"
                            v-model="formData.isAllScenario"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.isAllScenario"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="Number(item.dictKey)"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <!-- <el-col :span="12">
                    <el-form-item label="所属区域" prop="areaCode">
                        <el-tree-select
                            class="gd-select"
                            popper-class="gd-tree-select-popper"
                            v-model="formData.areaCode"
                            node-key="id"
                            :default-expanded-keys="expandedKeys"
                            :props="treeSelectProps"
                            placeholder="请选择"
                            clearable
                            filterable
                            :check-strictly="true"
                            lazy
                            :load="loadRegionNode"
                            :render-after-expand="false"
                        />
                    </el-form-item>
                </el-col> -->
            </el-row>
        </el-form>
        <template #footer v-if="!dialogReadonly">
            <el-button color="#F2F3F5" @click="visible = false">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button class="save-btn" color="#4C34FF" :loading="submitting" :disabled="submitting" @click="handleSubmit">
                保存
            </el-button>
        </template>
    </el-dialog>
</template>
<script setup>
import { getDictLabel } from '@ztzf/utils'
import { computed, ref, inject } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldRules } from '@ztzf/utils'
import { materialsSubmitApi, materialsDetailApi, regionLazyTreeApi } from './materialsApi'
// 初始化表单数据
const initForm = () => ({
    materialName: '',
    materialDesc: '',
    necessity: '',
    isAllScenario: '',
    areaCode: '',
    id: null,
})
const dictObj = inject('dictObj')
const emit = defineEmits(['success'])
const formRef = ref(null) // 表单实例
const formData = ref(initForm()) // 表单数据
const visible = defineModel() // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const dialogReadonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const expandedKeys = ref([]) // 树形选择器展开的节点
const regionNodeCache = ref(new Map()) // 缓存区域节点及其父节点关系
// 树形选择器配置
const treeSelectProps = ref({
    value: 'id',
    label: 'name',
    children: 'children',
    // 判断叶子节点
    isLeaf: (data, node) => {
        return node.level >= 2
    },
})
// 表单校验规则
const rules = {
    materialName: fieldRules(true),
    materialDesc: fieldRules(true),
    necessity: fieldRules(true),
    isAllScenario: fieldRules(true),
    areaCode: fieldRules(true),
}
// 提交新增/编辑
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
    try {
        await materialsSubmitApi(formData.value)
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
}
// 加载详情
async function loadDetail() {
    if (!formData.value.id) return
    const res = await materialsDetailApi({ id: formData.value.id })
    const data = res?.data?.data ?? {}
    // 将 areaCode 转换为字符串类型
    if (data.areaCode !== undefined) {
        data.areaCode = String(data.areaCode)
    }
    formData.value = data
    // 如果是编辑模式,获取区域节点的路径以展开树形选择器
    if (dialogMode.value === 'edit' && data.areaCode) {
        await loadRegionPath(data.areaCode)
    }
}
// 加载区域节点路径
async function loadRegionPath(areaCode) {
    try {
        const path = []
        const queue = [{ code: '360800000000', currentPath: ['360800000000'] }]
        let found = false
        // 广度优先搜索,逐层加载节点直到找到目标节点
        while (queue.length > 0 && !found) {
            const { code, currentPath } = queue.shift()
            // 获取当前节点的子节点
            const res = await regionLazyTreeApi({ parentCode: code })
            const nodes = res?.data?.data || []
            for (const node of nodes) {
                const nodeId = node.id || node.value
                // 缓存节点及其父节点关系
                regionNodeCache.value.set(nodeId, {
                    id: nodeId,
                    name: node.name || node.title || node.label,
                    parentId: code,
                    hasChildren: node.hasChildren || false,
                })
                // 检查是否找到目标节点
                if (nodeId === areaCode) {
                    path.push(...currentPath, nodeId)
                    found = true
                    break
                }
                // 如果有子节点,加入队列继续搜索
                if (node.hasChildren) {
                    queue.push({
                        code: nodeId,
                        currentPath: [...currentPath, nodeId],
                    })
                }
            }
        }
        if (path.length > 0) {
            expandedKeys.value = path
        }
    } catch (error) {
        console.error('加载区域路径失败:', error)
    }
}
// 打开弹框
async function open({ mode = 'add', row } = {}) {
    dialogMode.value = mode
    formData.value = dialogMode.value === 'add' ? initForm() : row
    if (dialogMode.value !== 'add') {
        await loadDetail()
    }
}
// 懒加载获取区域节点数据
async function loadRegionNode(node, resolve) {
    try {
        // 限制只加载两级,当前节点 level >= 1 时不加载子节点
        if (node.level >= 2) {
            resolve([])
            return
        }
        // 初始化加载时传递 code 参数,加载子节点时传递 parentCode 参数
        const params = node.data?.id ? { parentCode: node.data.id } : { code: '360800000000' }
        const parentCode = node.data?.id || '360800000000'
        const res = await regionLazyTreeApi(params)
        const nodes = res?.data?.data || []
        // 处理返回的节点数据
        const processedNodes = nodes.map(item => {
            const nodeId = item.id || item.value
            // 缓存节点及其父节点关系
            regionNodeCache.value.set(nodeId, {
                id: nodeId,
                name: item.name || item.title || item.label,
                parentId: parentCode,
                hasChildren: item.hasChildren || false,
            })
            return {
                id: nodeId,
                name: item.name || item.title || item.label,
                hasChildren: item.hasChildren || false,
            }
        })
        resolve(processedNodes)
    } catch (error) {
        console.error('获取区域数据失败:', error)
        resolve([])
    }
}
defineExpose({ open })
</script>
<style scoped lang="scss"></style>
applications/task-work-order/src/views/orderView/appConfiguration/materials/index.vue
New file
@@ -0,0 +1,230 @@
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="gd-search-form">
            <el-form-item label="材料名称" prop="materialName">
                <el-input
                    class="gd-input gray"
                    v-model="searchParams.materialName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <!-- <el-form-item label="所属区划" prop="areaCode">
                <el-tree-select
                    class="gd-select gray"
                    popper-class="gd-tree-select-popper"
                    v-model="searchParams.areaCode"
                    node-key="id"
                    :props="treeSelectProps"
                    placeholder="请选择"
                    clearable
                    filterable
                    @change="handleSearch"
                    :check-strictly="true"
                    lazy
                    :load="loadRegionNode"
                    :render-after-expand="false"
                />
            </el-form-item> -->
            <el-form-item class="gd-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="gd-table-toolbar">
            <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增材料</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">
                    <el-table-column label="序号" width="80">
                        <template v-slot="{ $index }">
                            {{ $index + 1 }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="materialName" show-overflow-tooltip label="材料名称" />
                    <el-table-column prop="materialDesc" show-overflow-tooltip label="材料说明" />
                    <el-table-column prop="necessity" show-overflow-tooltip label="必要性">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.necessity, dictObj.necessity) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="isAllScenario" show-overflow-tooltip label="是否适用全部情形">
                        <template v-slot="{ row }">
                            {{ getDictLabel(String(row.isAllScenario), dictObj.isAllScenario) }}
                        </template>
                    </el-table-column>
                    <!-- <el-table-column prop="areaName" show-overflow-tooltip label="所属区域" /> -->
                    <el-table-column label="操作" class-name="operation-btns" width="200">
                        <template v-slot="{ row }">
                            <el-link type="primary" @click="openForm('view', row)">查看</el-link>
                            <el-link type="primary" @click="openForm('edit', row)">编辑</el-link>
                            <el-link type="primary" @click="handleDelete(row)">删除</el-link>
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <div class="gd-pagination-parent">
                <el-pagination
                    popper-class="gd-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 ref="dialogRef" @success="getList" v-if="dialogVisible" v-model="dialogVisible" />
    </basic-container>
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { onMounted, ref, provide, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import FormDiaLog from './FormDiaLog.vue'
import { materialsPageApi, materialsRemoveApi, regionLazyTreeApi } from './materialsApi'
import { getDictLabel } from '@ztzf/utils'
// 初始化查询参数
const initSearchParams = () => ({
    materialName: '', // 材料名称
    areaCode: '', // 所属区划
    current: 1, // 当前页
    size: 10, // 每页大小
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
const dialogVisible = ref(false)
const dictObj = ref({
    necessity: [],
    isAllScenario: [],
}) // 字典对象
// 树形选择器配置
const treeSelectProps = {
    value: 'id',
    label: 'name',
    children: 'children',
    // 判断叶子节点
    isLeaf: (data, node) => {
        return data.leaf === true
    },
}
provide('dictObj', dictObj)
// 获取字典列表
function getDictList() {
    // 必要性字典:必填/可选/容缺
    dictObj.value.necessity = [
        { dictKey: '必填', dictValue: '必填' },
        { dictKey: '可选', dictValue: '可选' },
        { dictKey: '容缺', dictValue: '容缺' },
    ]
    // 是否适用全部情形字典
    dictObj.value.isAllScenario = [
        { dictKey: '1', dictValue: '是' },
        { dictKey: '0', dictValue: '否' },
    ]
}
// 懒加载获取区域节点数据
async function loadRegionNode(node, resolve) {
    try {
        // 限制只加载两级,当前节点 level >= 1 时不加载子节点
        if (node.level >= 2) {
            resolve([])
            return
        }
        // 初始化加载时传递 code 参数,加载子节点时传递 parentCode 参数
        const params = node.data?.id ? { parentCode: node.data.id } : { code: '360800000000' }
        const res = await regionLazyTreeApi(params)
        const nodes = res?.data?.data || []
        // 处理返回的节点数据
        const processedNodes = nodes.map(item => {
            // 判断是否为叶子节点:如果是第二级(node.level === 1)则为叶子节点
            const isLeaf = node.level === 1
            return {
                id: item.id || item.value,
                name: item.name || item.title || item.label,
                hasChildren: item.hasChildren || false,
                leaf: isLeaf,
            }
        })
        resolve(processedNodes)
    } catch (error) {
        console.error('获取区域数据失败:', error)
        resolve([])
    }
}
// 获取列表
async function getList() {
    loading.value = true
    try {
        const res = await materialsPageApi(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()
}
// 新增/编辑/查看 弹框
function openForm(mode, row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode, row })
    })
}
// 删除
async function handleDelete(row) {
    await ElMessageBox.confirm('确认删除该条记录吗?', '提示', {
        type: 'warning',
        customClass: 'gd-confirm-custom',
        confirmButtonClass: 'gd-confirm-button',
        cancelButtonClass: 'gd-confirm-cancel-button',
    })
    await materialsRemoveApi({ ids: row.id })
    ElMessage.success('删除成功')
    getList()
}
onMounted(() => {
    getDictList()
    getList()
})
</script>
<style scoped lang="scss"></style>
applications/task-work-order/src/views/orderView/appConfiguration/materials/materialsApi.js
New file
@@ -0,0 +1,46 @@
import request from '@/axios'
// 分页查询材料列表
export const materialsPageApi = params => {
    return request({
        url: `/drone-gd/implement/gdMaterial/page`,
        method: 'get',
        params: { ...params },
    })
}
// 获取材料详情
export const materialsDetailApi = params => {
    return request({
        url: `/drone-gd/implement/gdMaterial/detail`,
        method: 'get',
        params,
    })
}
// 新增或编辑材料
export const materialsSubmitApi = data => {
    return request({
        url: `/drone-gd/implement/gdMaterial/submit`,
        method: 'post',
        data,
    })
}
// 删除材料
export const materialsRemoveApi = params => {
    return request({
        url: `/drone-gd/implement/gdMaterial/remove`,
        method: 'post',
        params,
    })
}
// 懒加载区域树
export const regionLazyTreeApi = params => {
    return request({
        url: `/blade-system/region/lazy-tree`,
        method: 'get',
        params,
    })
}
applications/task-work-order/src/views/orderView/appConfiguration/materials/接口文档.txt
New file
@@ -0,0 +1,321 @@
## 分页
**接口地址**:`/implement/gdMaterial/page`
**请求方式**:`GET`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>传入gdMaterial</p>
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|areaCode|区域编码|query|false|string||
|current|当前页|query|false|integer(int32)||
|id|主键id|query|false|integer(int64)||
|isAllScenario|是否适用全部情形:1是 0否|query|false|integer(int32)||
|materialDesc|材料说明|query|false|string||
|materialName|材料名称(列表展示字段)|query|false|string||
|necessity|必要性:必填/可选/容缺|query|false|string||
|size|每页的数量|query|false|integer(int32)||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R«IPage«GdMaterialVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|IPage«GdMaterialVO»|IPage«GdMaterialVO»|
|&emsp;&emsp;current||integer(int64)||
|&emsp;&emsp;pages||integer(int64)||
|&emsp;&emsp;records||array|GdMaterialVO|
|&emsp;&emsp;&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;&emsp;&emsp;id|主键id|integer||
|&emsp;&emsp;&emsp;&emsp;isAllScenario|是否适用全部情形:1是 0否|integer||
|&emsp;&emsp;&emsp;&emsp;materialDesc|材料说明|string||
|&emsp;&emsp;&emsp;&emsp;materialName|材料名称(列表展示字段)|string||
|&emsp;&emsp;&emsp;&emsp;necessity|必要性:必填/可选/容缺|string||
|&emsp;&emsp;size||integer(int64)||
|&emsp;&emsp;total||integer(int64)||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {
        "current": 0,
        "pages": 0,
        "records": [
            {
                "areaCode": "",
                "id": 0,
                "isAllScenario": 0,
                "materialDesc": "",
                "materialName": "",
                "necessity": ""
            }
        ],
        "size": 0,
        "total": 0
    },
    "msg": "",
    "success": true
}
```
## 详情
**接口地址**:`/implement/gdMaterial/detail`
**请求方式**:`GET`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>传入gdMaterial</p>
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|id|主键|query|true|integer(int64)||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R«GdMaterialVO»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|GdMaterialVO|GdMaterialVO|
|&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;id|主键id|integer(int64)||
|&emsp;&emsp;isAllScenario|是否适用全部情形:1是 0否|integer(int32)||
|&emsp;&emsp;materialDesc|材料说明|string||
|&emsp;&emsp;materialName|材料名称(列表展示字段)|string||
|&emsp;&emsp;necessity|必要性:必填/可选/容缺|string||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {
        "areaCode": "",
        "id": 0,
        "isAllScenario": 0,
        "materialDesc": "",
        "materialName": "",
        "necessity": ""
    },
    "msg": "",
    "success": true
}
```
## 新增或修改
**接口地址**:`/implement/gdMaterial/submit`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入gdMaterial</p>
**请求示例**:
```javascript
{
  "areaCode": "",
  "id": 0,
  "isAllScenario": 0,
  "materialDesc": "",
  "materialName": "",
  "necessity": ""
}
```
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|gdMaterial|gdMaterial|body|true|GdMaterialDTO|GdMaterialDTO|
|&emsp;&emsp;areaCode|区域编码||false|string||
|&emsp;&emsp;id|主键id||false|integer(int64)||
|&emsp;&emsp;isAllScenario|是否适用全部情形:1是 0否||false|integer(int32)||
|&emsp;&emsp;materialDesc|材料说明||false|string||
|&emsp;&emsp;materialName|材料名称(列表展示字段)||false|string||
|&emsp;&emsp;necessity|必要性:必填/可选/容缺||false|string||
**响应状态**:
| 状态码 | 说明 | 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
}
```
## 逻辑删除
**接口地址**:`/implement/gdMaterial/remove`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入ids</p>
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|ids|主键集合|query|true|string||
**响应状态**:
| 状态码 | 说明 | 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
}
```
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
@@ -8,6 +8,7 @@
        :close-on-click-modal="false"
    >
        <div class="detail-container  detail-container1" v-if="dialogReadonly">
            <div class="detail-title">异议详情</div>
            <el-row class="detail-row-view">
                <el-col :span="12">
                    <div class="label">标题</div>
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/dataObjectionApi.js
@@ -45,3 +45,12 @@
    })
}
export const getDeptTreeListApi = tenantId => {
    return request({
        url: '/blade-system/dept/tree',
        method: 'get',
        params: {
            tenantId,
        },
    });
};
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue
@@ -98,7 +98,7 @@
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { onMounted, ref, provide, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDeptTree } from '@/api/system/dept'
import { getDeptTreeListApi } from './dataObjectionApi'
import { getDictLabel } from '@ztzf/utils'
import FormDiaLog from './FormDiaLog.vue'
import {
@@ -125,7 +125,7 @@
const deptTree = ref([]) // 部门树
const dictObj = ref({}) // 字典对象
const treeProps = {
    label: 'name',
    label: 'title',
    children: 'children',
}
const detailObjectionStatus = ref('')
@@ -183,7 +183,7 @@
// 获取部门树
function getDeptTreeFun() {
    getDeptTree().then(res => {
    getDeptTreeListApi().then(res => {
        deptTree.value = res.data.data
    })
}
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue
@@ -2,7 +2,7 @@
    <el-dialog
        class="gd-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        :title="dialogTitle"
        @closed="visible = false"
        destroy-on-close
        :close-on-click-modal="false"
@@ -76,7 +76,6 @@
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const dialogReadonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const rules = {
    rejectReason: fieldRules(true, 200),
@@ -99,12 +98,14 @@
}
let formLabelStr = ref('原因')
let dialogTitle = ref('新增')
// 操作类型:1.同意签收、2.拒绝签收 、3.撤回任务 、4.同意审核、 5.拒绝审核、 6.验收通过、 7.验收拒绝
async function open({ mode = 'add', row, formLabel, type } = {}) {
async function open({ mode = 'add', row, formLabel, type,title } = {}) {
    dialogMode.value = mode
    formLabelStr.value = formLabel
    dialogTitle.value =  dialogMode.value === 'add' ? title : '查看'
    formData.value = {
        id: row.id,
        auditStatus: type,
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -235,7 +235,7 @@
            </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>
                <el-button v-if="taskStatus === '3'" @click="statusChange(4)" color="#4C34FF">通过</el-button>
            </template>
        </template>
        <RefuseOrderDialog1
@@ -300,10 +300,10 @@
const getAirName = inject('getAirName')
const gdStatusObj = {
    '0': { reason: '拒绝原因', operationType: '2' },
    '0': { reason: '拒绝原因', operationType: '2',title:'拒绝签收' },
    '1': { reason: '拒绝原因' },
    '2': { reason: '' },
    '3': { reason: '驳回原因', operationType: '5' },
    '3': { reason: '驳回原因', operationType: '5',title:'驳回' },
    '4': { reason: '驳回原因' },
    '5': { reason: '' },
    '6': { reason: '拒绝原因' },
@@ -337,6 +337,7 @@
            row: formData.value,
            type: gdStatusObj[taskStatus.value].operationType,
            formLabel: gdStatusObj[taskStatus.value].reason,
            title: gdStatusObj[taskStatus.value]?.title
        })
    })
}
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
@@ -9,10 +9,10 @@
        :close-on-click-modal="false"
    >
        <div class="content" style="display: flex">
            <div class="processBox" v-if="dialogMode !== 'add' && processList.length">
            <div class="processBox" v-if="dialogMode !== 'add'">
                <div class="detail-title">工单记录</div>
                <div class="process" >
                    <OrderStepBar :processList="processList"/>
                <div class="process">
                    <OrderStepBar :processList="processList" />
                </div>
            </div>
            <div class="leftBox">
@@ -269,7 +269,7 @@
import { gdManageDeviceListApi } from './gdManageDeviceApi'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import * as Cesium from 'cesium'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import { DrawPolygon } from '@ztzf/utils'
import { pxToRem } from '@/utils/rem'
import dayjs from 'dayjs'
import RefuseOrderDialog from '@/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue'
@@ -514,11 +514,17 @@
    viewPlane = viewer.entities?.add({
        customType: 'control_group',
        position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
        polygon: {
            hierarchy: Cesium.Cartesian3.fromDegreesArray(result),
            material: Cesium.Color.fromBytes(45, 140, 240, 99),
            outline: false,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        },
        polyline: {
            positions: Cesium.Cartesian3.fromDegreesArray(result),
            clampToGround: true,
            width: 3,
            material: Cesium.Color.RED,
            width: 2,
            material: Cesium.Color.fromBytes(45, 140, 240, 255),
        },
    })
    flyVisual({
@@ -679,7 +685,7 @@
        display: flex;
        flex-direction: column;
        .process{
        .process {
            height: 100%;
            padding: 12px 16px 24px 16px;
            width: 312px;
applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue
@@ -58,7 +58,7 @@
<script setup>
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldRules, getDictLabel } from '@ztzf/utils'
import { fieldRules } from '@ztzf/utils'
import { gdWorkOrderHandleStatusApi } from '@/views/orderView/orderManage/orderManage/orderManageApi'
// 初始化表单数据
@@ -75,7 +75,6 @@
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const dialogReadonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const rules = {
    rejectReason: fieldRules(true, 200),
applications/task-work-order/src/views/resource/components/spotDetails.vue
File was deleted
applications/task-work-order/src/views/resource/patchManagement.vue
File was deleted
packages/utils/map/DrawPolygon.js
File was renamed from applications/task-work-order/src/utils/cesium/DrawPolygon.js
@@ -1,9 +1,8 @@
import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
import { boxTransformScale } from '@/utils/turfFunc'
import { ElMessage } from 'element-plus'
import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
import { flyVisual } from '@ztzf/utils'
import { flyVisual, getPointPositionsHeight } from './index'
import { MapTooltip } from './MapTooltip'
/**
 * 多边形绘制与编辑工具类
@@ -65,6 +64,8 @@
        this.delPoint = this.delPoint.bind(this)
        // 外部订阅者
        this.listeners = []
        // 绘制提示 tooltip
        this.drawingTooltip = null
    }
    // 实体命名常量
@@ -433,6 +434,11 @@
        const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
        if (!cartesian) return
        // 更新绘制提示 tooltip 位置
        if (this.drawingMode && this.drawingTooltip) {
            this.drawingTooltip.move(movement.endPosition)
        }
        // 编辑模式下,拖拽点实时更新
        if (this.editingMode && this.draggedEntity?.customData) {
            this.draggedEntity.position = cartesian
@@ -581,6 +587,11 @@
    finishDrawing() {
        this.curPolygon.positions.pop()
        // 隐藏绘制提示
        if (this.drawingTooltip) {
            this.drawingTooltip.hide()
        }
        if (this.curPolygon.positions.length >= 3) {
            this.drawingMode = false
            this.editingMode = true
@@ -609,6 +620,11 @@
        this.removeMenuPopup()
        this.notify('getPolygonPositions', [])
        this.startDrawing()
        // 重新显示绘制提示
        if (this.drawingTooltip) {
            this.drawingTooltip.show('左键点击绘制,双击结束绘制')
        }
    }
    // 删除图斑
    delSpot() {
@@ -727,6 +743,15 @@
            this.addPosition(item, false)
        })
        const boxTransformScale = (data, multiple = 3) => {
            const line = turf.lineString(data)
            const bbox = turf.bbox(line)
            const bboxPolygon = turf.bboxPolygon(bbox)
            const scaledPolygon = turf.transformScale(bboxPolygon, multiple)
            return turf.bbox(scaledPolygon)
        }
        // 视角飞入区域
        const newBox = boxTransformScale(
            positions.map(item => [item.lng, item.lat]),
@@ -752,6 +777,12 @@
    initHandler(viewer) {
        this.viewer = viewer
        this.startDrawing()
        // 初始化绘制提示 tooltip
        if (!this.drawingTooltip) {
            this.drawingTooltip = new MapTooltip(viewer)
            this.drawingTooltip.show('左键点击绘制,双击结束绘制')
        }
        if (!this.handler) {
            this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
@@ -800,5 +831,11 @@
        this.removeEntities()
        this.removeHandler()
        this.enableMapControl()
        // 销毁绘制提示 tooltip
        if (this.drawingTooltip) {
            this.drawingTooltip.destroy()
            this.drawingTooltip = null
        }
    }
}
packages/utils/map/index.js
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium'
import { MapTooltip } from './MapTooltip'
import { DrawPolygon } from './DrawPolygon'
/**
 * 飞到中心点并且所有点在可视范围
 *  @param positionsData 二维数组,每项为 [lon, lat] 或 三维数组 [lon, lat, height]
@@ -61,4 +62,46 @@
    })
}
export { MapTooltip, flyVisual }
// 批量获取点数组经纬度对应高度信息
const getPointPositionsHeight = (data, viewer, droneHeight = null) => {
    return new Promise((resolve, reject) => {
        if (!data || !data.length) {
            resolve([])
            return
        }
        // 假设 viewer 已经初始化并且 terrainProvider 是有效的
        const terrainProvider = viewer?.terrainProvider
        // 创建 Cartographic 对象
        const cartographics = data.map(item => {
            const { lng, lat } = item
            return Cesium.Cartographic.fromDegrees(Number(lng), Number(lat))
        })
        // 获取地形数据的Promise
        const promise = Cesium.sampleTerrainMostDetailed(terrainProvider, cartographics)
        // 使用 Cesium.when 处理 Promise
        promise
            .then(function (updatedPositions) {
                //  是一个数组,包含更新后的 Cartographic 对象
                const newPosition = updatedPositions.map((item, index) => {
                    const longitude = Cesium.Math.toDegrees(item.longitude)
                    const latitude = Cesium.Math.toDegrees(item.latitude)
                    let pointData = {
                        ...data[index],
                        longitude,
                        latitude,
                        ASL: Number(item.height),
                        customHeight: Number(item.height),
                    }
                    if (droneHeight) pointData.TH = Number(item.height) + Number(droneHeight)
                    return pointData
                })
                resolve(newPosition)
            })
            .catch(function (error) {
                console.log('获取高程时发生错误:', error)
                resolve(data.map(item => ({ ...item, ASL: 0, customHeight: 0, longitude: item.lng, latitude: item.lat })))
            })
    })
}
export { MapTooltip, flyVisual, getPointPositionsHeight, DrawPolygon }
packages/utils/package.json
@@ -1,18 +1,22 @@
{
  "name": "@ztzf/utils",
  "version": "1.0.0",
  "private": true,
  "description": "工具类",
  "main": "index.js",
  "license": "ISC",
  "peerDependencies": {
    "cesium": "catalog:",
    "dayjs": "catalog:",
    "decimal.js": "catalog:"
  },
  "devDependencies": {
    "cesium": "catalog:",
    "dayjs": "catalog:",
    "decimal.js": "catalog:"
  }
    "name": "@ztzf/utils",
    "version": "1.0.0",
    "private": true,
    "description": "工具类",
    "main": "index.js",
    "license": "ISC",
    "peerDependencies": {
        "@turf/turf": "catalog:",
        "cesium": "catalog:",
        "dayjs": "catalog:",
        "decimal.js": "catalog:",
        "element-plus": "catalog:"
    },
    "devDependencies": {
        "@turf/turf": "catalog:",
        "cesium": "catalog:",
        "dayjs": "catalog:",
        "decimal.js": "catalog:",
        "element-plus": "catalog:"
    }
}
pnpm-lock.yaml
@@ -878,6 +878,12 @@
  packages/utils:
    devDependencies:
      '@turf/turf':
        specifier: 'catalog:'
        version: 6.5.0
      '@ztzf/utils':
        specifier: workspace:*
        version: 'link:'
      cesium:
        specifier: 'catalog:'
        version: 1.126.0
@@ -887,6 +893,9 @@
      decimal.js:
        specifier: 'catalog:'
        version: 10.6.0
      element-plus:
        specifier: 'catalog:'
        version: 2.9.11(vue@3.5.27(typescript@5.9.3))
  uniapps/work-app:
    dependencies:
uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -155,10 +155,10 @@
    const smallUrl = detail.eventImageUrl
    imageArr.push(smallUrl)
  }
  if (detail.update_photo_url) {
    const smallUrl = detail.update_photo_url
    imageArr.push(smallUrl)
  }
  // if (detail.update_photo_url) {
  //   const smallUrl = detail.update_photo_url
  //   imageArr.push(smallUrl)
  // }
  return imageArr
})
// 跳转地图
@@ -277,7 +277,7 @@
            title: workDetailData.value.event_name || '工单详情',
            summary: '查看工单详情',
            href: 'https://example.com/workDetail?eventNum=' + workDetailData.value.event_num,
            imageUrl: workDetailData.value.photo_url,
            imageUrl: workDetailData.value.eventImageUrl,
            success: () => {
                uni.showToast({ title: '分享成功', icon: 'success' })
                onShareModalClose()