吉安感知网项目-前端
罗广辉
2026-01-16 8049ae0a3c7ea0cf8e3fe0b4adf32f98439b8591
Merge remote-tracking branch 'origin/master'
21 files modified
1 files renamed
40 files added
85 files deleted
47865 ■■■■ changed files
applications/drone-command/src/styles/common/cockpit.scss 3 ●●●● patch | view | raw | blame | history
applications/task-work-order/env/.env.development 12 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/api/dataCockpit/index.js 54 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/Frame.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/fly.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/flying.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/full-charge.svg 13 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/low-battery.svg 6 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/medium-battery.svg 6 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/assets/images/aiNowFly/starting.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/equipment.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/fzsb.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/layerControl.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/left-btn.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/map/equipment.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/right-btn.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/title-bg.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/warning.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/dataCockpit/zcsb.png patch | view | raw | blame | history
applications/task-work-order/src/assets/images/no-data-bg.png patch | view | raw | blame | history
applications/task-work-order/src/components/map-container/device-map-container.vue 437 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/map-container/mapContainer-copy.vue 10 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/map-container/mapContainer.vue 2 ●●● patch | view | raw | blame | history
applications/task-work-order/src/config/website.js 20 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/router/views/index.js 11 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/store/modules/user.js 4 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/DrawPolygon.js 301 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/Material/index.js 18 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/common.js 8 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/compressImage.js 55 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/compressImage2.js 46 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/createRouteLine.js 923 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/eventBus.js 5 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/frustum/CesiumVideoFrustum.js 712 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/frustum/CoordinateTranslate.js 229 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/kmz.js 10 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/mapUtil.js 207 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/publicCesium.js 168 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/use-kmz-tsa.js 675 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/utils/cesium/useBoundary.js 210 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/RoutePlan/PlanarAirLine/planarRouteUtils.js 1018 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/RoutePlan/PointAirLine/pointWayLineUtils.js 613 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/algorithmMange/algorithmMange.vue 322 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/algorithmRepository/algorithmRepository.vue 381 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/authority/apiscope.vue 645 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/authority/datascope.vue 737 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/authority/role.vue 440 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/base/region.vue 457 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCenter/components/dataCenterMap.vue 341 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCenter/components/searchData.vue 499 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCenter/dataCenter.vue 1225 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/CenterContainer.vue 237 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/DeviceHistoryDialog.vue 208 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/EmptyState.vue 59 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/EquipmentWarning.vue 101 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/HistoryWarning.vue 123 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/LeftContainer.vue 219 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/RealWarning.vue 109 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/RightContainer.vue 288 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue 171 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue 149 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue 213 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/templateComponents/RealTemplate.vue 245 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/components/templateComponents/TitleTemplate.vue 43 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/dataCockpit/index.vue 96 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/desk/notice.vue 307 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/addDevice.vue 688 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/airport.vue 1687 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/DeviceSettingBox.vue 433 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/DeviceSettingPopover.vue 99 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/DockControlPanel.vue 552 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/devicePerShare.vue 301 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/firmwareManage.vue 444 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/use-device-setting.js 72 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/components/use-dock-control.js 70 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/devicePerMenu.vue 307 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/fly.vue 537 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/device/index.vue 97 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/flow/deploy.vue 160 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/flow/follow.vue 199 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/flow/manager.vue 369 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/flow/model.vue 548 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/CancelTaskDialog.vue 152 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/DeviceJobDetails.vue 806 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/DeviceJobDetailsMap.vue 364 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/FileTypePlay.vue 158 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/JobRelatedEvents.vue 392 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/SearchBox.vue 590 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskAlgorithmBusiness.vue 131 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskIntermediateContent/AddTask.vue 414 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue 382 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskIntermediateContent/TaskTable.vue 252 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskTop/TaskEvent.vue 155 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskTop/TaskIndustry.vue 117 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskTop/TaskTime.vue 161 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskTop/TaskTop.vue 47 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/TaskTop/TaskTotal.vue 157 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/components/executeNestDetails.vue 225 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/const/drc.js 9 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/jobinfo.vue 269 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/jobserver.vue 213 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/job/jobstatistics.vue 14 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/layerManagement/components/folderFile.vue 316 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/layerManagement/components/leftList.vue 588 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/layerManagement/components/nationalSpatialPlanning.vue 49 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/layerManagement/components/rightEdit.vue 471 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/layerManagement/index.vue 602 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/license/license.vue 360 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/license/upload.vue 112 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/monitor/log/api.vue 218 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/monitor/log/error.vue 173 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/monitor/log/flightLog.vue 529 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/monitor/log/usual.vue 164 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/odm/task.vue 492 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/report/reportlist.vue 199 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/resource/components/spotDetails.vue 2 ●●● patch | view | raw | blame | history
applications/task-work-order/src/views/system/menu.vue 4 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/system/user/FormDiaLog.vue 451 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/system/user/index.vue 504 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/test.vue 9 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/component/AddEditDetails.vue 622 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/component/SearchBox.vue 313 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/orderLog.vue 528 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/orderLogCopy.vue 1666 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticket-copy.vue 4072 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticket.vue 2358 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticketComponent/CreateTicketDialog.vue 713 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticketComponent/DispatchDialog.vue 244 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticketComponent/RecheckDialog.vue 117 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tickets/ticketComponent/TicketDetailDialog.vue 1441 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tool/code.vue 659 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tool/datasource.vue 375 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/tool/model.vue 393 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/backlog.vue 434 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/calendarBox.vue 398 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/flightStatistics.vue 524 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/flyratio.vue 357 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/proportionStatic.vue 395 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/statistics.vue 498 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/components/taskOutcome.vue 315 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/dashboard.vue 196 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/wel/index.vue 212 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/workspace/index.vue 303 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/hooks/useGlobalWS.js 102 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/login/index.vue 53 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/workDetail/index.vue 366 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/websocket.js 6 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common/cockpit.scss
@@ -1303,7 +1303,8 @@
                    .ztzf-table-container {
                        height: 360px;
                        margin-top: 0;
                        margin-bottom: 20px;
                        .ztzf-table-pagination {
                            justify-content: center;
applications/task-work-order/env/.env.development
@@ -1,3 +1,13 @@
###
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-16 11:02:24
 # @FilePath     : \applications\task-work-order\env\.env.development
 # @Description  :
 # Copyright 2026 OBKoro1, All Rights Reserved.
 # 2026-01-07 14:58:30
###
NODE_ENV = 'development'
#开发环境配置
@@ -5,7 +15,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.33:81
VITE_APP_URL= http://192.168.1.204
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/task-work-order/src/api/dataCockpit/index.js
New file
@@ -0,0 +1,54 @@
/*
 * @Author       : yuan
 * @Date         : 2026-01-09 11:14:04
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 11:35:14
 * @FilePath     : \applications\drone-command\src\api\dataCockpit\index.js
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-09 11:14:04
 */
import request from '@/axios'
// 设备查询
export const deviceSearchApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/deviceSearch',
        method: 'get',
        params
    })
}
// 历史告警/实时告警
export const alarmLogApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/alarmLog',
        method: 'get',
        params
    })
}
// 告警记录统计
export const alarmStatisticsApi = () => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/alarmStatistics',
        method: 'get'
    })
}
// 信号干扰/诱导驱离
export const interferenceAndExpulsionApi = (data) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/interferenceAndExpulsion',
        method: 'post',
        data
    })
}
// 设备统计
export const deviceStatisticsApi = () => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/deviceStatistics',
        method: 'get'
    })
}
applications/task-work-order/src/assets/images/aiNowFly/Frame.png
applications/task-work-order/src/assets/images/aiNowFly/fly.png
applications/task-work-order/src/assets/images/aiNowFly/flying.png
applications/task-work-order/src/assets/images/aiNowFly/full-charge.svg
New file
@@ -0,0 +1,13 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="url(#paint0_linear_1519_1955)"/>
<path d="M22.909 42.9474V8.33691H5.09082V42.9474H22.909Z" fill="#40FF5C"/>
</g>
<defs>
<linearGradient id="paint0_linear_1519_1955" x1="14" y1="0" x2="14" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0.25" stop-color="#00FFF2"/>
<stop offset="0.514423" stop-color="#00FFF2"/>
<stop offset="0.721154" stop-color="#00FFF2"/>
</linearGradient>
</defs>
</svg>
applications/task-work-order/src/assets/images/aiNowFly/low-battery.svg
New file
@@ -0,0 +1,6 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 1321316190">
<path id="Vector" d="M16.3293 0C16.4942 0 16.6576 0.03249 16.81 0.0956148C16.9624 0.15874 17.1008 0.251263 17.2175 0.367902C17.3341 0.484542 17.4266 0.623013 17.4897 0.77541C17.5529 0.927807 17.5854 1.09114 17.5854 1.2561V2.5122H23.8659C24.8653 2.5122 25.8237 2.90921 26.5304 3.6159C27.2371 4.32259 27.6341 5.28107 27.6341 6.28049V43.9634C27.6341 44.9628 27.2371 45.9213 26.5304 46.628C25.8237 47.3347 24.8653 47.7317 23.8659 47.7317H3.76829C2.76888 47.7317 1.8104 47.3347 1.10371 46.628C0.397016 45.9213 0 44.9628 0 43.9634V6.28049C0 5.28107 0.397016 4.32259 1.10371 3.6159C1.8104 2.90921 2.76888 2.5122 3.76829 2.5122H10.0488V1.2561C10.0488 0.92296 10.1811 0.603466 10.4167 0.367902C10.6522 0.132338 10.9717 0 11.3049 0H16.3293ZM23.8659 5.02439H3.76829C3.43515 5.02439 3.11566 5.15673 2.8801 5.39229C2.64453 5.62786 2.5122 5.94735 2.5122 6.28049V43.9634C2.5122 44.2966 2.64453 44.616 2.8801 44.8516C3.11566 45.0872 3.43515 45.2195 3.76829 45.2195H23.8659C24.199 45.2195 24.5185 45.0872 24.754 44.8516C24.9896 44.616 25.122 44.2966 25.122 43.9634V6.28049C25.122 5.94735 24.9896 5.62786 24.754 5.39229C24.5185 5.15673 24.199 5.02439 23.8659 5.02439Z" fill="#00FFF2"/>
<path id="Vector_2" d="M22.6098 42.707V37.6826H5.02441V42.707H22.6098Z" fill="#F20303"/>
</g>
</svg>
applications/task-work-order/src/assets/images/aiNowFly/medium-battery.svg
New file
@@ -0,0 +1,6 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="#00FFF2"/>
<path d="M22.9091 42.9474V23.4947H5.09091V42.9474H22.9091Z" fill="#00EEFF"/>
</g>
</svg>
applications/task-work-order/src/assets/images/aiNowFly/starting.png
applications/task-work-order/src/assets/images/dataCockpit/equipment.png
applications/task-work-order/src/assets/images/dataCockpit/fzsb.png
applications/task-work-order/src/assets/images/dataCockpit/layerControl.png
applications/task-work-order/src/assets/images/dataCockpit/left-btn.png
applications/task-work-order/src/assets/images/dataCockpit/map/equipment.png
applications/task-work-order/src/assets/images/dataCockpit/right-btn.png
applications/task-work-order/src/assets/images/dataCockpit/title-bg.png
applications/task-work-order/src/assets/images/dataCockpit/warning.png
applications/task-work-order/src/assets/images/dataCockpit/zcsb.png
applications/task-work-order/src/assets/images/no-data-bg.png
applications/task-work-order/src/components/map-container/device-map-container.vue
New file
@@ -0,0 +1,437 @@
<template>
    <div class="ztzf-cesium map-container" :id="props.containerId"></div>
    <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
        <div class="layer-control-wrap" ref="layerWrapRef">
            <div class="layer-control" @click="toggleLayerPanel">
                <img :src="layerControlIcon" alt="图层控制" />
            </div>
            <div v-if="showLayerPanel" class="layer-panel">
                <div class="panel-title">图层管理</div>
                <div class="panel-content">
                    <el-tree
                        :data="layerTree"
                        show-checkbox
                        default-expand-all
                        node-key="key"
                        :props="layerTreeProps"
                        :default-checked-keys="defaultCheckedKeys"
                    />
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
import * as Cesium from 'cesium'
import { PublicCesium } from '@/utils/cesium/publicCesium'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
const props = defineProps({
    onlineDevices: {
        type: Array,
        default: () => [],
    },
    leftCollapsed: {
        type: Boolean,
        default: false,
    },
    containerId: {
        type: String,
        default: 'device-map-container',
    },
    showLayerControl: {
        type: Boolean,
        default: true,
    },
})
let viewInstance = null
let viewer = null
const deviceEntityIds = new Set()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const layerTreeProps = {
    label: 'label',
    children: 'children',
}
const defaultCheckedKeys = ['global', 'city-base']
const layerTree = ref([
    {
        key: 'base',
        label: '地理信息图层',
        children: [
            { key: 'global', label: '全球地形' },
            { key: 'admin', label: '行政区划' },
        ],
    },
    {
        key: 'city',
        label: '城市CIM图层',
        children: [
            { key: 'city-base', label: '皖山白模' },
            { key: 'city-grid', label: '皖山白模光栅网格' },
            { key: 'city-tilt', label: '皖山倾斜摄影' },
            { key: 'city-tilt-grid', label: '皖山倾斜摄影网格' },
        ],
    },
    {
        key: 'sky',
        label: '空域要素图层',
        children: [
            { key: 'airspace', label: '空域边界' },
            { key: 'route', label: '飞行航路' },
        ],
    },
])
const getDevicePosition = item => {
    const longitudeRaw = item.longitude ?? item.lng ?? item.lon
    const latitudeRaw = item.latitude ?? item.lat
    if (longitudeRaw == null || latitudeRaw == null) return null
    const longitude = Number(longitudeRaw)
    const latitude = Number(latitudeRaw)
    if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
    return { longitude, latitude }
}
const clearDeviceEntities = () => {
    if (!viewer) return
    deviceEntityIds.forEach(id => {
        viewer.entities.removeById(id)
    })
    deviceEntityIds.clear()
}
const RING_STYLES = [
    { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] },
    { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] },
    { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] },
]
const MATERIAL_TYPE = 'RadialGradientMaterial'
let materialRegistered = false
const registerRadialGradientMaterial = () => {
    if (materialRegistered || !Cesium?.Material) return
    materialRegistered = true
    Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
        fabric: {
            type: MATERIAL_TYPE,
            uniforms: {
                color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
                color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0),
            },
            source: `
                czm_material czm_getMaterial(czm_materialInput materialInput) {
                    czm_material material = czm_getDefaultMaterial(materialInput);
                    vec2 st = materialInput.st - vec2(0.5);
                    float t = clamp(length(st) * 2.0, 0.0, 1.0);
                    vec4 color = mix(color1, color2, t);
                    material.diffuse = color.rgb;
                    material.alpha = color.a;
                    return material;
                }
            `,
        },
        translucent: () => true,
    })
}
const buildCirclePositions = (center, radiusMeters, steps = 64) => {
    const positions = []
    const latRad = Cesium.Math.toRadians(center.latitude)
    const metersToLat = 1 / 111320
    const metersToLon = 1 / (111320 * Math.cos(latRad))
    for (let i = 0; i <= steps; i += 1) {
        const angle = Cesium.Math.toRadians((i / steps) * 360)
        const dx = radiusMeters * Math.cos(angle)
        const dy = radiusMeters * Math.sin(angle)
        const lon = center.longitude + dx * metersToLon
        const lat = center.latitude + dy * metersToLat
        positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
    }
    return positions
}
const addDeviceRings = (center, baseId) => {
    RING_STYLES.forEach((ring, index) => {
        const outerPositions = buildCirclePositions(center, ring.outer)
        const holes = ring.inner
            ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
            : []
        const entityId = `${baseId}-ring-${index}`
        deviceEntityIds.add(entityId)
        registerRadialGradientMaterial()
        const [startColor, endColor] = ring.gradient
        const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
        const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
        const material = new RadialGradientMaterialProperty(color1, color2)
        viewer.entities.add({
            id: entityId,
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
                material,
            },
        })
    })
}
class RadialGradientMaterialProperty {
    constructor(color1, color2) {
        this._definitionChanged = new Cesium.Event()
        this.color1 = color1
        this.color2 = color2
    }
    get isConstant() {
        return true
    }
    get definitionChanged() {
        return this._definitionChanged
    }
    getType() {
        return MATERIAL_TYPE
    }
    getValue(time, result) {
        const target = result || {}
        target.color1 = this.color1
        target.color2 = this.color2
        return target
    }
    equals(other) {
        return (
            other instanceof RadialGradientMaterialProperty &&
            Cesium.Color.equals(this.color1, other.color1) &&
            Cesium.Color.equals(this.color2, other.color2)
        )
    }
}
const renderDeviceEntities = devices => {
    if (!viewer) return
    clearDeviceEntities()
    devices.forEach((item, index) => {
        const position = getDevicePosition(item)
        if (!position) return
        const entityId = `online-device-${item.id ?? index}`
        deviceEntityIds.add(entityId)
        addDeviceRings(position, entityId)
        viewer.entities.add({
            id: entityId,
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            billboard: {
                image: equipmentIcon,
                width: 40.34,
                height: 40.34,
            },
        })
    })
}
watch(
    () => props.onlineDevices,
    devices => {
        renderDeviceEntities(devices || [])
    },
    { deep: true }
)
watch(
    () => props.leftCollapsed,
    isCollapsed => {
        if (isCollapsed) showLayerPanel.value = false
    }
)
const toggleLayerPanel = () => {
    if (!props.showLayerControl) return
    showLayerPanel.value = !showLayerPanel.value
}
const handleClickOutside = event => {
    if (!showLayerPanel.value) return
    const target = event.target
    if (layerWrapRef.value?.contains(target)) return
    showLayerPanel.value = false
}
onMounted(() => {
    document.addEventListener('click', handleClickOutside)
    viewInstance = new PublicCesium({
        dom: props.containerId,
        flatMode: false,
        terrain: false,
        layerMode: 4,
        contour: false,
    })
    viewer = viewInstance.getViewer()
    renderDeviceEntities(props.onlineDevices)
})
onBeforeUnmount(() => {
    document.removeEventListener('click', handleClickOutside)
    clearDeviceEntities()
    if (viewInstance) {
        viewInstance?.viewerDestroy()
        viewInstance = null
    }
    viewer = null
})
</script>
<style lang="scss" scoped>
.map-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
.layer-control-root {
    position: absolute;
    left: 411px;
    bottom: 22px;
    z-index: 9;
    transition: transform 0.3s ease-in-out;
    pointer-events: none;
    &.collapsed {
        transform: translateX(-401px);
    }
}
.layer-control-wrap {
    position: relative;
    display: flex;
    align-items: flex-end;
    pointer-events: auto;
}
.layer-control {
    width: 34px;
    height: 34px;
    cursor: pointer;
    img {
        width: 100%;
        height: 100%;
        display: block;
    }
}
.layer-panel {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 46px;
    bottom: 0;
    width: 160px;
    max-height: 442px;
    background: #191932;
    border-radius: 8px 8px 8px 8px;
    .panel-title {
        padding: 0 16px;
        line-height: 42px;
        font-family: 'Open Sans', Open Sans;
        font-weight: 400;
        font-size: 12px;
        color: #ffffff;
        text-align: left;
        font-style: normal;
        text-transform: none;
        border-bottom: 1px solid rgba(70, 70, 100, 0.5);
        box-sizing: border-box;
    }
    .panel-content {
        padding: 0 16px;
        height: 0;
        flex: 1;
        overflow: auto;
    }
    ::v-deep(.el-tree) {
        background: transparent;
        color: #c3c3dd;
        font-size: 12px;
        .el-tree-node {
            line-height: 30px !important;
            .el-tree-node__content {
                display: flex;
                align-items: center;
                height: 40px !important;
                line-height: 40px !important;
                border-bottom: 1px solid rgba(70, 70, 100, 0.5);
                box-sizing: border-box;
            }
            .el-tree-node__children {
                .el-tree-node__content {
                    border: none;
                }
            }
        }
    }
}
.layer-panel :deep(.el-tree-node__label) {
    color: #c3c3dd;
}
.layer-panel :deep(.el-tree-node__expand-icon) {
    order: 3;
    margin-left: auto;
}
.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
    visibility: hidden;
}
.layer-panel :deep(.el-checkbox) {
    order: 1;
}
.layer-panel :deep(.el-tree-node__label) {
    order: 2;
}
.layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) {
    display: none;
}
.layer-panel :deep(.el-tree-node__content:hover) {
    background: transparent !important;
}
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
    background: transparent !important;
    color: #ffffff;
}
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
    color: #ffffff;
}
.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
    background-color: #023aff;
    border-color: #023aff;
}
.layer-panel :deep(.el-checkbox__inner) {
    background-color: transparent;
    border-color: #a1a3d4;
}
</style>
applications/task-work-order/src/components/map-container/mapContainer-copy.vue
@@ -3,10 +3,10 @@
 * @Date: 2024-10-25 15:07:51
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-28 11:34:40
 * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue
 * @Description:
 *
 * Copyright (c) 2024 by shuishen, All Rights Reserved.
 * @FilePath: \drone-command\src\components\map-container\mapContainer.vue
 * @Description:
 *
 * Copyright (c) 2024 by shuishen, All Rights Reserved.
-->
<template>
  <div class="map-container">
@@ -261,4 +261,4 @@
  height: 120%;
  transform: translate(-50%, -50%);
}
</style>
</style>
applications/task-work-order/src/components/map-container/mapContainer.vue
@@ -3,7 +3,7 @@
 * @Date: 2024-10-25 15:07:51
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-28 11:34:40
 * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue
 * @FilePath: \drone-command\src\components\map-container\mapContainer.vue
 * @Description:
 *
 * Copyright (c) 2024 by shuishen, All Rights Reserved.
applications/task-work-order/src/config/website.js
@@ -1,13 +1,23 @@
/*
 * @Author       : yuan
 * @Date         : 2026-01-06 09:47:27
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-16 11:12:30
 * @FilePath     : \applications\task-work-order\src\config\website.js
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 09:47:27
 */
/**
 * 全局配置文件
 */
export default {
  title: 'saber',
  logo: 'S',
  key: 'saber', //配置主键,目前用于存储
  key: 'work', //配置主键,目前用于存储
  indexTitle: '',
  clientId: 'drone', // 客户端id
  clientSecret: 'drone_secret', // 客户端密钥
  clientId: 'saber', // 客户端id
  clientSecret: 'saber_secret', // 客户端密钥
  tenantMode: true, // 是否开启租户模式
  tenantId: '000000', // 管理组租户编号
  captchaMode: true, // 是否开启验证码模式
@@ -32,8 +42,8 @@
    menu: true,
  },
  fistPage: {
    name: '个人工作台',
    path: '/wel/index',
    name: '数据驾驶舱',
    path: '/dataCockpit/index',
    // path: '/tickets/ticket',
  },
applications/task-work-order/src/router/views/index.js
@@ -15,16 +15,7 @@
          i18n: 'dashboard',
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
      },
      {
        path: 'dashboard',
        name: '控制台',
        meta: {
          i18n: 'dashboard',
          menu: false,
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
      },
      }
    ],
  },
  // 事件工单
applications/task-work-order/src/store/modules/user.js
@@ -280,7 +280,7 @@
        })
      }
      return new Promise(resolve => {
        getRoutes({tenantId,sysType: 1}).then(res => {
        getRoutes({tenantId,sysType: 5}).then(res => {
          const data = res.data.data;
          let menu = deepClone(data);
          if (!res.data?.data?.length){
@@ -299,7 +299,7 @@
    },
    GetButtons({ commit }) {
      return new Promise(resolve => {
        getButtons({sysType: 1}).then(res => {
        getButtons({sysType: 5}).then(res => {
          const data = res.data.data;
          commit('SET_PERMISSION', data);
          resolve();
applications/task-work-order/src/utils/cesium/DrawPolygon.js
File was renamed from applications/task-work-order/src/views/layerManagement/components/utils.js
@@ -1,7 +1,8 @@
import * as Cesium from 'cesium'
import _, { cloneDeep, throttle } from 'lodash'
import * as turf from '@turf/turf'
import { boxTransformScale } from '@/utils/turfFunc'
import { ElMessage } from 'element-plus'
import { flyVisual, getPointPositionsHeight } from '@/utils/cesium/mapUtil'
/**
 * 多边形绘制与编辑工具类
@@ -14,8 +15,6 @@
 */
export class DrawPolygon {
    constructor() {
        // 是否绘制
        this.whetherToDraw =false
        // 图斑预览模式标记
        this.isPureSpotPreview = false
        // 是否删除测区
@@ -39,6 +38,8 @@
        // 存储端点的 DataSource
        this.editPolygonDataSource = null
        this.editPolygonPointDataSource = null
        // 存储中点(边中点)的 DataSource:用于编辑态插入新端点
        this.editPolygonMidPointDataSource = null
        // 鼠标事件处理器
        this.handler = null
        // 右键菜单 DOM
@@ -63,15 +64,17 @@
        this.delPoint = this.delPoint.bind(this)
        // 外部订阅者
        this.listeners = []
    }
    // 实体命名常量
    static ENTITY_NAMES = {
        POLYGON: '区域-面',
        POINT: '区域-端点',
        // 编辑态中点(用于快速插入新端点)
        MID_POINT: '区域-中点',
        POLYGON_ID: 'planar-route-polygon',
        POINT_ID_PREFIX: 'planar-route-point',
        MID_POINT_ID_PREFIX: 'planar-route-mid-point',
    }
    // 颜色常量
@@ -98,10 +101,20 @@
    // 编辑图斑
    editThePatch(data) {
        this.isPreviewMode = data
    }
    // 绘制
    drawTheArea(data){
        this.whetherToDraw = data
        // 关闭编辑能力时隐藏端点/中点,并清空中点,避免残留交互
        if (!this.isPreviewMode) {
            this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = false)
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false)
            this.editPolygonMidPointDataSource?.entities?.removeAll?.()
            return
        }
        if (this.editingMode) {
            this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true)
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
            this.rebuildEditPoints()
            this.rebuildMidPoints()
        }
    }
    // 删除测区
    deleteTheArea(data) {
@@ -110,12 +123,6 @@
    // 开始绘制
    startDrawing() {
        if (!this.whetherToDraw) {
            return;
          }
        this.drawingMode = true
        this.curPolygon = new Cesium.PolygonHierarchy()
@@ -130,9 +137,16 @@
            this.viewer?.dataSources.add(this.editPolygonPointDataSource)
        }
        // 中点数据源:编辑模式下用于“边上插点”
        if (!this.editPolygonMidPointDataSource) {
            this.editPolygonMidPointDataSource = new Cesium.CustomDataSource('editPolygonMidPointDataSource')
            this.viewer?.dataSources.add(this.editPolygonMidPointDataSource)
        }
        // 清空之前的点
        this.editPolygonDataSource?.entities.removeAll()
        this.editPolygonPointDataSource?.entities.removeAll()
        this.editPolygonMidPointDataSource?.entities.removeAll()
    }
    // 创建多边形实体(含边界线)
@@ -177,10 +191,162 @@
                ind: pointIndex, // 索引记录
            },
        })
    }
    createMidPointEntity(startInd, position) {
        // 使用 CallbackProperty 让中点位置随端点拖拽实时计算
        // startInd 表示该中点属于边:(startInd) -> (startInd + 1)
        const updatePosition = () => {
            const positions = this.curPolygon?.positions
            if (!positions || positions.length < 2) return position
            const n = positions.length
            const p1 = positions[startInd]
            const p2 = positions[(startInd + 1) % n]
            if (!p1 || !p2) return position
            return Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3())
        }
        this.editPolygonMidPointDataSource.entities.add({
            name: DrawPolygon.ENTITY_NAMES.MID_POINT,
            id: `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${startInd}`,
            position: new Cesium.CallbackProperty(updatePosition, false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromCssColorString('rgba(255, 255, 255, .7)'),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customData: {
                startInd,
            },
        })
    }
    rebuildEditPoints() {
        // 插入/删除端点后:端点实体 id 与 customData.ind 需要全量重建,保证索引与 positions 一致
        if (!this.isPreviewMode) return
        if (!this.editPolygonPointDataSource || !this.curPolygon?.positions) return
        this.editPolygonPointDataSource.entities.removeAll()
        this.curPolygon.positions.forEach((position, index) => {
            this.editPolygonPointDataSource.entities.add({
                name: DrawPolygon.ENTITY_NAMES.POINT,
                id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${index}`,
                position: position.clone(),
                point: {
                    pixelSize: 14,
                    color: Cesium.Color.WHITE,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    ind: index,
                },
            })
        })
    }
    rebuildMidPoints() {
        // 仅在“顶点数量变化/切换编辑态”时维护中点实体数量
        // 拖拽过程中中点位置由 CallbackProperty 自动更新,不需要重建
        if (!this.isPreviewMode) return
        if (!this.editPolygonMidPointDataSource || !this.curPolygon?.positions) return
        const positions = this.curPolygon.positions
        const n = positions.length
        const entities = this.editPolygonMidPointDataSource.entities
        if (!this.editingMode || n < 2) {
            entities.removeAll()
            return
        }
        const neededIds = new Set()
        for (let i = 0; i < n; i += 1) {
            const id = `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${i}`
            neededIds.add(id)
            // 缺少则补齐:保证每条边都有一个中点实体
            if (!entities.getById(id)) {
                const p1 = positions[i]
                const p2 = positions[(i + 1) % n]
                if (!p1 || !p2) continue
                const mid = Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3())
                this.createMidPointEntity(i, mid)
            }
        }
        entities.values.slice().forEach(entity => {
            if (entity?.name !== DrawPolygon.ENTITY_NAMES.MID_POINT) return
            // 多余则移除:例如删除端点导致边数量减少
            if (!neededIds.has(entity.id)) {
                entities.remove(entity)
            }
        })
    }
    insertPointFromMidPoint(midPointEntity) {
        // 点击中点:在对应边上插入一个新端点(白点),并立即进入拖拽态
        if (!this.isPreviewMode) return
        if (!this.editingMode) return
        if (!midPointEntity?.customData) return
        const startInd = midPointEntity.customData.startInd
        // 中点位置可能是 CallbackProperty,需要取当前时刻的值
        const positionProperty = midPointEntity.position
        const entityPosition = positionProperty?.getValue
            ? positionProperty.getValue(Cesium.JulianDate.now())
            : positionProperty
        if (!entityPosition) return
        // 插入到 startInd 与 startInd+1 之间
        const insertIndex = Math.min(Math.max(startInd + 1, 0), this.curPolygon.positions.length)
        this.curPolygon.positions.splice(insertIndex, 0, entityPosition.clone ? entityPosition.clone() : entityPosition)
        this.rebuildEditPoints()
        this.rebuildMidPoints()
        const newPointEntity = this.editPolygonPointDataSource?.entities?.getById(
            `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${insertIndex}`
        )
        if (newPointEntity) {
            this.isDragging = true
            this.draggedEntity = newPointEntity
            this.currentDragPointPosition = this.curPolygon.positions[insertIndex]
            this.disableMapControl()
        }
        this.notify('getPolygonPositions', this.curPolygon.positions)
    }
    // 清除不在范围内的点
    removeLastInvalidPoint() {
        const posLen = this.curPolygon.positions.length
        if (posLen === 0) return
        let targetPointId = ''
        let removeCount = 0
        if (posLen === 2) {
            removeCount = 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}0`
        } else if (posLen > 2) {
            removeCount = 1
            const pointIndex = posLen - 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`
        }
        this.curPolygon.positions.splice(posLen - removeCount, removeCount)
        const invalidPoint = this.editPolygonPointDataSource.entities.getById(targetPointId)
        if (invalidPoint) {
            this.editPolygonPointDataSource.entities.remove(invalidPoint)
        }
        if (this.curPolygon.positions.length === 0 && this.polygonEntity) {
            this.editPolygonDataSource.entities.remove(this.polygonEntity)
            this.polygonEntity = null
        }
    }
    // 添加一个点
    addPosition(position, isAdd = true) {
        // 第一个点要重复压入一次,形成动态绘制效果
@@ -211,12 +377,18 @@
        const pickedEntity = this.viewer.scene.pick(movement.position)?.id
        const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT
        const isMidPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.MID_POINT
        if (pickedEntity && isPoint) {
            this.isDragging = true
            this.draggedEntity = pickedEntity
            this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind]
            this.disableMapControl() // 禁止地图交互
            return
        }
        if (pickedEntity && isMidPoint) {
            this.insertPointFromMidPoint(pickedEntity)
        }
    }
@@ -249,6 +421,7 @@
            this.isDragging = false
            this.draggedEntity = null
            this.enableMapControl()
            this.rebuildMidPoints()
        }
    }
@@ -262,7 +435,7 @@
        // 编辑模式下,拖拽点实时更新
        if (this.editingMode && this.draggedEntity?.customData) {
            this.draggedEntity.position = cartesian
            this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian
            this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian
        }
        // 绘制模式下,实时更新最后一个点
@@ -293,27 +466,28 @@
    // 鼠标左键点击
    handleLeftClick(click) {
        this.removeMenuPopup()
        const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id)
        const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点')
        const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
        const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
        if (this.drawingMode && !this.whetherToDraw) {
            return; // 阻断后续绘制逻辑
          }
        if (isStartPoint) return
        // 如果不是绘制模式
        if (!this.drawingMode) {
            if (!isPolygon) {
                this.editingMode = false
                this.editPolygonPointDataSource.entities.show = false
                this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false)
                return
            }
            if (isPolygon) {
                this.editingMode = true
                this.editPolygonPointDataSource.entities.show = true
                this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
                this.rebuildMidPoints()
            }
            return
        }
@@ -334,13 +508,22 @@
        const cartesian = this.viewer.scene.pickPosition(click.position)
        if (!cartesian) return
        let arr = [...this.curPolygon.positions]
        arr.pop()
        // 校验多边形是否自交
        if (arr.length > 2 && !this.curDragPointIsValid([...arr, cartesian])) {
            ElMessage.warning('测区不支持交叉绘制,请重新绘制')
            return
        }
        this.addPosition(cartesian)
    }
    // 鼠标右键点击(弹出菜单)
    handleRightClick(click) {
    handleRightClick(click) {
        const that = this
        if (that.drawingMode) return
        that.removeMenuPopup()
        const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id)
@@ -351,16 +534,14 @@
        } = click
        let pickedEntity, tooltipEvent, menuType
        if (isPolygon) {
            pickedEntity = isPolygon
            tooltipEvent = that.delPolygon
            menuType = 'polygon'
        } else if (isEditPoint) {
        if (isEditPoint) {
            pickedEntity = isEditPoint
            tooltipEvent = that.delPoint
            menuType = 'edit-point'
        } else if (isPolygon) {
            pickedEntity = isPolygon
            tooltipEvent = that.delPolygon
            menuType = 'polygon'
        }
        if (pickedEntity && this.isDeleteTheArea) {
@@ -385,6 +566,11 @@
            this.editPolygonPointDataSource.entities.removeAll()
            this.editPolygonPointDataSource = null
        }
        if (this.editPolygonMidPointDataSource) {
            this.editPolygonMidPointDataSource.entities.removeAll()
            this.editPolygonMidPointDataSource = null
        }
        this.editingMode = false
        this.polygonEntity = null
        this.curPolygon = null
@@ -398,6 +584,8 @@
            this.drawingMode = false
            this.editingMode = true
            this.editPolygonPointDataSource.entities.show = true
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
            this.rebuildMidPoints()
            if (!this.curDragPointIsValid(this.curPolygon.positions)) {
                this.isShowWaringTip = true
@@ -421,7 +609,12 @@
        this.notify('getPolygonPositions', [])
        this.startDrawing()
    }
    // 删除图斑
    delSpot() {
        this.removeEntities()
        this.isPreviewMode = true
        this.startDrawing()
    }
    // 删除端点
    delPoint() {
        if (this.curPolygon.positions.length <= 3) {
@@ -431,13 +624,9 @@
        if (!this.delPolygonPoint) return
        this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1)
        this.editPolygonPointDataSource.entities.remove(this.delPolygonPoint)
        this.removeMenuPopup()
        // 更新剩余点索引
        this.editPolygonPointDataSource.entities.values.forEach((item, index) => {
            item.customData.ind = index
        })
        this.rebuildEditPoints()
        this.rebuildMidPoints()
        this.notify('getPolygonPositions', this.curPolygon.positions)
    }
@@ -454,7 +643,6 @@
        menuPopup.id = 'planarPolygonEditMenu'
        menuPopup.className = 'planar-polygon-edit-menu'
        const menuItems =
            type === 'polygon'
                ? [{ title: '删除测区', class: 'del-planar-polygon' }]
@@ -466,7 +654,6 @@
            titleDiv.className = item.class
            menuPopup.appendChild(titleDiv)
        })
        this.isPreviewMode = true
        menuPopupVBox.appendChild(menuPopup)
        return menuPopupVBox
@@ -522,23 +709,18 @@
        return intersections.features.length === 0
    }
    // 初始化已有多边形
    initPolygon(viewer, positions, isPurePreview = false) {
    async initPolygon(viewer, positions, isPurePreview = false) {
        this.initHandler(viewer)
        if (this.whetherToDraw) {
            this.startDrawing();
          } else {
            // 新增:不允许绘制时,确保绘制模式为 false,避免误触发
            this.drawingMode = false;
          }
        this.isPureSpotPreview = isPurePreview
        this.startDrawing()
        let newPosition = positions.map(item => {
            return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height))
            return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item?.height || 0))
        })
        // 预览航线的时候调用
        if (!this.isPureSpotPreview) {
            this.notify('getPolygonPositions', newPosition)
        }
        newPosition.forEach(item => {
            this.addPosition(item, false)
@@ -555,17 +737,20 @@
            duration: 0.5,
        })
        let pointList = await getPointPositionsHeight(positions, viewer)
        flyVisual({ positionsData: pointList.map(item => [item.lng, item.lat, item.ASL]), viewer })
        this.drawingMode = false
        this.editingMode = true
        this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true)
        this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
        this.rebuildMidPoints()
    }
    // 初始化事件处理器
    initHandler(viewer) {
        this.viewer = viewer
        if(this.whetherToDraw){
            this.startDrawing()
        }
        this.startDrawing()
        if (!this.handler) {
            this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
applications/task-work-order/src/utils/cesium/Material/index.js
@@ -54,6 +54,7 @@
    this._opacity = undefined
    this._alphaPower = undefined
    this._speed = undefined
    this._xRepeatCount = undefined
    this.color = options.color || Cesium.Color.fromBytes(0, 255, 255, 255)
    this.glowPower = .25
@@ -64,6 +65,9 @@
    this.opacity = options.opacity || 0.5
    this.alphaPower = options.alphaPower || 1.5
    this.speed = options.speed || 5
    this.xRepeatCount = options.xRepeatCount || 1
  }
  get isConstant () {
@@ -132,15 +136,17 @@
      color: new Cesium.Color(1.0, 1.0, 0.0, 0.7),
      bgColor: new Cesium.Color(0.0, 1.0, 0.0, 0.0),
      speed: 5,
      globalAlpha: 1.0
      globalAlpha: 1.0,
      xRepeatCount: 1.0
    },
    source: `
          uniform vec4 bgColor;
          uniform vec4 color;
          uniform float speed;
          uniform float globalAlpha;
          uniform float xRepeatCount; // 新增的 uniform 在着色器中
          czm_material czm_getMaterial(czm_materialInput materialInput) {
           czm_material czm_getMaterial(czm_materialInput materialInput) {
              czm_material material = czm_getDefaultMaterial(materialInput);
              vec2 st = materialInput.st;
              float time = fract(czm_frameNumber * speed / 1000.0);
@@ -150,7 +156,7 @@
                  colorMars3D = vec3(1.0);
              }
              material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s - time));
              material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s * xRepeatCount - time)); // 使用 xRepeatCount 来调整纹理重复
              material.diffuse = max(colorMars3D.rgb * material.alpha, colorMars3D.rgb);
              if (material.alpha < bgColor.a) {
@@ -182,6 +188,8 @@
    result.color = Cesium.Property.getValueOrUndefined(this._color, time)
    result.globalAlpha = Cesium.Property.getValueOrUndefined(this._opacity, time)
    result.speed = Cesium.Property.getValueOrUndefined(this._speed, time)
    result.xRepeatCount = Cesium.Property.getValueOrUndefined(this._xRepeatCount, time)
    return result
  }
@@ -191,7 +199,8 @@
      (other instanceof LineTrailMaterial &&
        Cesium.Property.equals(this._color, other._color) &&
        Cesium.Property.equals(this._opacity, other._opacity) &&
        Cesium.Property.equals(this._speed, other._speed))
        Cesium.Property.equals(this._speed, other._speed) &&
        Cesium.Property.equals(this._xRepeatCount, other._xRepeatCount))
    )
  }
}
@@ -200,6 +209,7 @@
  color: Cesium.createPropertyDescriptor('color'),
  opacity: Cesium.createPropertyDescriptor('opacity'),
  speed: Cesium.createPropertyDescriptor('speed'),
  xRepeatCount: Cesium.createPropertyDescriptor('xRepeatCount'),
})
applications/task-work-order/src/utils/cesium/common.js
@@ -131,22 +131,22 @@
    let newLat = lat
    switch (direction) {
        case 'W': // 向前(heading 方向)
        case 'KeyW': // 向前(heading 方向)
            newLon += moveStep * Math.sin(headingRad)
            newLat += moveStep * Math.cos(headingRad)
            break
        case 'S': // 向后(反方向)
        case 'KeyS': // 向后(反方向)
            newLon -= moveStep * Math.sin(headingRad)
            newLat -= moveStep * Math.cos(headingRad)
            break
        case 'D': // 向右(heading + 90°)
        case 'KeyD': // 向右(heading + 90°)
            newLon += moveStep * Math.cos(headingRad)
            newLat -= moveStep * Math.sin(headingRad)
            break
        case 'A': // 向左(heading - 90°)
        case 'KeyA': // 向左(heading - 90°)
            newLon -= moveStep * Math.cos(headingRad)
            newLat += moveStep * Math.sin(headingRad)
            break
applications/task-work-order/src/utils/cesium/compressImage.js
New file
@@ -0,0 +1,55 @@
/**
 * @description: 图片压缩
 * @param {*} src 图片地址
 * @param {*} quality 压缩质量
 * @param {*} maxWidth 最大宽度
 * @param {*} callback 回调函数
 * @return {*}
 */
function compressImage(src, quality, maxWidth, callback) {
    const img = new Image()
    img.setAttribute('crossOrigin', 'Anonymous')
    img.onload = function () {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        let width = img.width
        let height = img.height
        if (width > maxWidth) {
            height *= maxWidth / width
            width = maxWidth
        }
        canvas.width = width
        canvas.height = height
        ctx.drawImage(img, 0, 0, width, height)
        const newData = canvas.toDataURL('image/jpeg', quality)
        // 使用atob()将base64转换为二进制字符串
        const binaryString = window.atob(newData.split(',')[1])
        const mimeString = newData.split(',')[0].split(':')[1].split(';')[0]
        // 创建Blob对象
        const array = []
        for (let i = 0; i < binaryString.length; i++) {
            array.push(binaryString.charCodeAt(i))
        }
        const blob = new Blob([new Uint8Array(array)], { type: mimeString })
        callback(blob)
    }
    img.src = src
    return src
}
export default function imageCompress(src, quality, maxWidth) {
    // 使用例子
    return compressImage(src, quality, maxWidth, function (blob) {
        // 创建一个新的图片URL
        const imageUrl = URL.createObjectURL(blob)
        // 可以在这里使用imageUrl,比如设置为img元素的src
        // document.getElementById('your-image-element').src = imageUrl;
        // 当不需要这个URL的时候,释放它
        URL.revokeObjectURL(imageUrl)
    })
}
applications/task-work-order/src/utils/cesium/compressImage2.js
New file
@@ -0,0 +1,46 @@
/*
 * @Author: GuLiMmo 2820890765@qq.com
 * @Date: 2024-05-21 10:14:11
 * @LastEditors: GuLiMmo 2820890765@qq.com
 * @LastEditTime: 2024-05-21 10:20:34
 * @FilePath: /bigScreen/src/utils/cesium/compressImage2.js
 * @Description:
 * Copyright (c) 2024 by GuLiMmo, All Rights Reserved.
 */
export default function compressImage(
    src,
    maxWidth,
    maxHeight,
    outputFormat = 'image/jpeg',
    quality = 0.7
) {
    return new Promise((resolve, reject) => {
        const image = new Image()
        image.setAttribute('crossOrigin', 'Anonymous')
        image.src = src
        image.onload = () => {
            let width = image.width
            let height = image.height
            if (width > maxWidth) {
                height *= maxWidth / width
                width = maxWidth
            }
            if (height > maxHeight) {
                width *= maxHeight / height
                height = maxHeight
            }
            const canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height
            const ctx = canvas.getContext('2d')
            ctx.drawImage(image, 0, 0, width, height)
            const newDataUrl = canvas.toDataURL(outputFormat, quality)
            resolve(newDataUrl)
        }
        image.onerror = reject
    })
}
applications/task-work-order/src/utils/cesium/createRouteLine.js
@@ -2,17 +2,16 @@
import { Decimal } from 'decimal.js'
import _, { cloneDeep, throttle } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { render } from 'vue'
import { analysisPointLineKmz, handlePointListForKmz } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils'
import HistoryDronePopup from '@/components/DeviceJobDetails/HistoryDronePopup.vue'
import { getNewPolygonData } from '@/views/RoutePlan/PlanarAirLine/planarRouteUtils'
import {
    getNewPolygonData,
} from '@/views/newRoutePlan/routeUtils'
import { analyzeKmzFile, removeTextKey, XMLToJSON } from '@/utils/cesium/kmz'
import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial, } from '@/utils/cesium/Material'
import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial } from '@/utils/cesium/Material'
import CreateFrustum from '@/utils/cesium/frustum/CreateFrustum'
import { ElMessage } from 'element-plus'
@@ -23,61 +22,21 @@
import aircraftGltf from '@/assets/gltf/aircraft.gltf'
import newNumPoint from '@/assets/images/newStartPoint.png'
import {
    getPolyLine,
} from '@/views/RoutePlan/PointAirLine/pointWayLineUtils'
import { getPolyLine } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils'
import store from '@/store/index'
import newStartPoint from '@/assets/images/newStartPoint.png'
import newStartPointMore from '@/assets/images/newStartPointMore.png'
import {
    arrowLineMaterialProperty, arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT,
    arrowLineMaterialPropertyGreen,
    arrowLineMaterialPropertyOrange,
} from '@/hooks/useMorePointLine/useMorePointLine'
let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
    color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5
})
let arrowLineMaterialPropertyOrange = new ArrowLineMaterialProperty({
    color: new Cesium.Color(255 / 255, 185 / 255, 58 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
let arrowLineMaterialPropertyGreen = new ArrowLineMaterialProperty({
    color: new Cesium.Color(6 / 255, 217 / 255, 87 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
// 记录index
let indexLog = 0
// 颜色
function getLineColor(lineNumber) {
    // 颜色顺序: 蓝(0), 黄(1), 绿(2)
    const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 3
    // 返回对应颜色
    return colors[colorIndex]
}
let runningLineMaterial = new LineTrailMaterial({
    color: Cesium.Color.fromCssColorString('#1FFF69'),
    opacity: 1,
    speed: 10,
})
let runningTextColor = Cesium.Color.fromCssColorString('#1FFF69')
let runningTextOffset = new Cesium.Cartesian2(0, -32)
let pendingLineMaterial = new PolylineGlowMaterial({
    color: Cesium.Color.fromCssColorString('#FFB81D'),
})
let pendingTextColor = Cesium.Color.fromCssColorString('#FFB81D')
let pendingTextOffset = new Cesium.Cartesian2(0, 32)
let MapPopUpBox = HistoryDronePopup
function getZoomFactor (camerasInfo, type) {
@@ -95,10 +54,108 @@
    return zoom_factor
}
// 记录index
let indexLog = 0
// 颜色
function getPendingExecutionLineColor (lineNumber) {
    // 颜色顺序: 蓝(灰), 更灰
    const colors = [arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 2
    // 返回对应颜色
    return colors[colorIndex]
}
function getLineColor (lineNumber) {
    // 颜色顺序: 蓝(0), 黄(1), 绿(2)
    const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 3
    // 返回对应颜色
    return colors[colorIndex]
}
function getBase64Image (imgUrl) {
    const img = new Image()
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    return new Promise(resolve => {
        img.onload = function () {
            canvas.width = img.width
            canvas.height = img.height
            ctx.drawImage(img, 0, 0)
            const base64 = canvas.toDataURL('image/png')
            resolve(base64)
        }
        img.src = imgUrl
    })
}
async function createCombinedSVG (index, billboardImageUrl) {
    const base64Image = await getBase64Image(billboardImageUrl)
    const svg = `
      <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
        <image href="${base64Image}" x="10" y="10" width="80" height="80"/>
        <text x="50" y="54" font-size="26" fill="white" text-anchor="middle" dominant-baseline="middle">
          ${index + 1}
        </text>
      </svg>
    `
    return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}
function findHeightByCoordinates (data, targetLng, targetLat) {
    // 展平二维数组,然后查找第一个匹配的点
    const allPoints = data.flat()
    const point = allPoints.find(point =>
        point.longitude === targetLng && point.latitude === targetLat
    )
    return point ? point.height : null
}
function renderingMachineNest (options) {
    const { viewer, dockTransformPosition, dockPosition, data } = options
    viewer.entities.add({
        name: 'work-drone-route-point-dock',
        position: dockTransformPosition,
        billboard: {
            image: endingOnlineImg,
            width: 50,
            height: 50,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
        },
        customData: {
            data,
        },
    })
    viewer.entities.add({
        name: 'work-drone-route--point-polyline',
        position: dockTransformPosition,
        polyline: getPolyLine(viewer, { value: [dockPosition] }, 0),
    })
}
function renderingStartingPoint (options) {
    const { viewer, startPosition, data } = options
    viewer.entities.add({
        name: 'work-drone-route-point-start',
        position: startPosition,
        billboard: {
            image: rwqfdImg,
            width: 50,
            height: 50,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
        },
        customData: {
            data,
        },
    })
}
export default class CreateRouteLine {
    /**
     *
     *
     * @param {*} options 参数
     */
    constructor(options) {
@@ -119,10 +176,9 @@
    /**
     * 初始化viewer
     * @param {*} viewer
     * @param {*} viewer
     */
    initCreateRoute (viewer) {
        indexLog = 0
        this.viewer = viewer
    }
@@ -138,24 +194,27 @@
            const currentWaypointIndex = output?.ext['current_waypoint_index']
            this.updataMultiNestData(sn, {
                currentWaypointIndex
                currentWaypointIndex,
            })
        }
    }
    /**
     * 请求航线文件数据并解析处理
     * 请求航线文件数据并绘制解析处理
     * @param {*} data 包含文件url等信息
     * @param {*} dronePosition 无人机位置
     * @returns
     * @param {*} dockPosition 无人机位置
     * @returns
     */
    async parsingFiles (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) {
    async parsingFiles (data, dockPosition = null, isShowDock = false, isShowPointBillboard = true) {
        console.log(data,'888')
        const { url, wayline_type, device_sn } = data
        if (!url) return dronePosition != null ? {
            dronePosition: [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }]
        } : {}
        indexLog = 0
        if (!url)
            return dockPosition != null
                ? {
                    dronePosition: [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }],
                }
                : {}
        this.updataMultiNestData(device_sn)
        const res = await analyzeKmzFile(`${url}?_t=${new Date().getTime()}`)
@@ -178,16 +237,16 @@
        const missionConfig = removeTextKey(waylinesXmlJson.missionConfig)
        const waylinesXMLObj = removeTextKey(waylinesXmlJson.Folder)
        if (templateType === "mapping3d") {
        if (templateType === 'mapping3d') {
            if (!waylinesXMLObj[0].Placemark.length) return ElMessage.error('没有航线点位')
        } else {
            if (!waylinesXMLObj.Placemark.length) return ElMessage.error('没有航线点位')
        }
        if ([2, 4, 5, 6, 7, 10].includes(Number(wayline_type))) {
            if (templateType === "mapping3d") {
        if ([2, 4, 5, 7, 10].includes(Number(wayline_type))) {
            if (templateType === 'mapping3d') {
                let morePolygonData = waylinesXMLObj.map(i => ({
                    data: i.Placemark
                    data: i.Placemark,
                }))
                let newPolygonData = getNewPolygonData(morePolygonData)
@@ -214,7 +273,7 @@
                    waylinesXMLObj[0],
                    missionConfig,
                    templateXmlJson,
                    dronePosition,
                    dockPosition,
                    data,
                    startPoint,
                    isShowDock,
@@ -227,7 +286,7 @@
                waylinesXMLObj,
                missionConfig,
                templateXmlJson,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -238,7 +297,7 @@
            return this.drawPointRoute(
                waylinesXMLObj,
                missionConfig,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -248,50 +307,231 @@
        }
    }
    // 拆分航线
    async splitWayLine (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) {
        const {
            pointPlacemark,
            polygonList,
            pointList,
            templateType,
            startPoint,
            execute_height_mode,
            auto_flight_speed,
            take_off_security_height,
            buffer_distance_meters,
            missionConfig,
            waylinesXMLObjR,
        } = await analysisPointLineKmz(`${data.domain_url}${data.object_key}`)
        const { positionArray, filePositions } = this.disposeData(
            waylinesXMLObjR,
            missionConfig,
            null,
            data,
            startPoint,
            0
        )
        let bigPointList = handlePointListForKmz({
            placemark: pointPlacemark,
            startPoint: startPoint,
            execute_height_mode: execute_height_mode,
            auto_flight_speed: auto_flight_speed,
        })
        // 路径线
        let entityConfig
        bigPointList.shift()
        let flyPositions = bigPointList.map(i => [Number(i.longitude), Number(i.latitude), Number(i.height)])
        // 渲染多个点
        let firstTopPosition = null
        let resultPosotions = []
        await Promise.all(data.wayline_file_list.map(async (item, index) => {
            // 小航线渲染线
            const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key
            const { pointList } = await analysisPointLineKmz(kmzUrl)
            resultPosotions.push(pointList)
        })).then(res => {
            // 目的是区分大航线点属于哪条小航线
            bigPointList.forEach(bigObj => {
                let foundIndex = -1
                // 遍历 resultPositions 查找匹配项
                resultPosotions.some((subArray, subIndex) => {
                    const found = subArray.some(obj =>
                        obj.latitude === bigObj.latitude &&
                        obj.longitude === bigObj.longitude
                    )
                    if (found) {
                        foundIndex = subIndex
                        return true // 找到就停止搜索
                    }
                    return false
                })
                // 如果找到了,给 bigPointList 的对象添加标记
                if (foundIndex !== -1) {
                    bigObj.log = foundIndex // 或 bigObj.index = foundIndex
                }
            })
            console.log(bigPointList, '查看结果值') // 现在 bigPointList 会有 log 属性
            bigPointList.map(async (i, index) => {
                firstTopPosition = i
                const position = Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(findHeightByCoordinates(resultPosotions, i.longitude, i.latitude)))
                const combinedImage = await createCombinedSVG(index, i.log % 2 ? newStartPoint : newStartPointMore)
                entityConfig = {
                    name: 'work-drone-route-point-split',
                    position: position,
                    billboard: {
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                        image: combinedImage,
                        width: 50,
                        height: 50,
                        verticalOrigin: Cesium.VerticalOrigin.CENTER,
                        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                    }
                }
                // 只在第一个点添加 label
                if (index === 0) {
                    entityConfig.label = {
                        text: data.task_name || '',
                        font: 'bold 16px sans-serif',
                        fillColor: data.status === 2 ? runningTextColor : pendingTextColor,
                        pixelOffset: data.status === 2 ? runningTextOffset : pendingTextOffset,
                        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                        outlineWidth: 2,
                        outlineColor: Cesium.Color.BLACK,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    }
                }
                this.viewer.entities.add(entityConfig)
            })
        })
        // 渲染航线
        data.wayline_file_list.map(async (item, index) => {
            // 小航线渲染线
            const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key
            const { pointList } = await analysisPointLineKmz(kmzUrl)
            // if (curRouteLineData.value.droneData && splitLines.length === 1) {
            //     // 把pointList的第一个点替换为无人机位置
            //     pointList[0] = curRouteLineData.value.droneData[0]
            // }
            if (take_off_security_height + pointList[0].height > firstTopPosition.height) {
                // 安全高度+机巢高度 > 第一个点高度
                const positionTest = {
                    longitude: pointList[0].longitude,
                    latitude: pointList[0].latitude,
                    height: take_off_security_height + pointList[0].height,
                }
                const positionTest1 = {
                    longitude: pointList[1].longitude,
                    latitude: pointList[1].latitude,
                    height: take_off_security_height + pointList[0].height,
                }
                pointList.splice(1, 0, positionTest)
                // 往数组中插入元素
                pointList.splice(2, 0, positionTest1)
                // console.log(pointList, 'pointList')
            } else if (take_off_security_height + pointList[0].height < firstTopPosition.height) {
                const positionTest = {
                    longitude: pointList[0].longitude,
                    latitude: pointList[0].latitude,
                    height: firstTopPosition.height,
                }
                pointList.splice(1, 0, positionTest)
            }
            const routePositions = pointList.map(i =>
                Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(i.height))
            )
            this.viewer.entities.add({
                name: 'work-drone-route-line-split',
                polyline: {
                    width: 4,
                    positions: routePositions,
                    material: data.status === 1 ? arrowLineMaterialPropertyGray : getLineColor(index + 1),
                    clampToGround: false,
                },
            })
        })
            // 落点线
            ; (bigPointList || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
                let setting = {
                    name: 'work-drone-route--planar-polyline-split',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: bigPointList }, index),
                }
                this.viewer.entities.add(setting)
            })
        return {
            entity: entityConfig,
            routeLinePositions: positionArray,
            filePositions:
                dronePosition != null
                    ? [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, ...filePositions]
                    : filePositions,
        }
    }
    /**
     * 绘制面状航线
     * @param {*} lineObj 航线文件中的
     * @param {*} missionConfig 航线文件中的
     * @param {*} templateXmlJson 航线文件中的
     * @param {*} dronePosition 无人机位置
     * @param {*} data
     * @param {*} startPoint
     * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注
     * @returns
     * @param {*} dockPosition 无人机位置
     * @param {*} data
     * @param {*} startPoint
     * @returns
     */
    async drawPlanarRoute (lineObj, missionConfig, templateXmlJson, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) {
        const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type)
    async drawPlanarRoute (
        lineObj,
        missionConfig,
        templateXmlJson,
        dockPosition,
        data,
        startPoint,
        isShowDock,
        isShowPointBillboard,
        wayline_type
    ) {
        const { positionArray, filePositions } = this.disposeData(
            lineObj,
            missionConfig,
            dockPosition,
            data,
            startPoint,
            wayline_type
        )
        const { device_sn } = data
        const droneTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dronePosition.longitude),
            Number(dronePosition.latitude),
            Number(dronePosition.height),
        const dockTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dockPosition.longitude),
            Number(dockPosition.latitude),
            Number(dockPosition.height)
        )
        let coordArr = null
        const placemark = templateXmlJson.Folder?.Placemark
        // 取出点位
        const coordinates =
            placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.[
                '#text'
            ]?.split('\n') || []
        const coordinates = placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.['#text']?.split('\n') || []
        // 数组转换
        coordArr = coordinates.map((coordinate) =>
        coordArr = coordinates.map(coordinate =>
            coordinate
                .replace(/\s+/g, '')
                .split(',')
                .map((v) => Number(v)),
                .map(v => Number(v))
        )
        // 获取当前经纬度绝对高度
        let newCoordArr = coordArr.map(item => ([item[0], item[1], 0]))
        let newCoordArr = coordArr.map(item => [item[0], item[1], 0])
        let planarRouteEntity = this.viewer.entities.add({
            name: 'work-drone-route-planar-polyline',
            polyline: {
                width: 3,
                positions: positionArray,
@@ -300,30 +540,30 @@
            },
            customData: {
                data
            }
                data,
            },
        })
        let droppointPositions = (filePositions || [])?.map(i => ({
            longitude: Number(i.lng),
            latitude: Number(i.lat),
            height: Number(i.alt),
        }));
        }))
        // 落点线
        (droppointPositions || [])?.forEach((item, index) => {
            const topPosition = Cesium.Cartesian3.fromDegrees(
                Number(item.longitude),
                Number(item.latitude),
                Number(item.height)
            )
            let setting = {
                name: 'work-drone-route-planar-polyline',
                position: topPosition,
                polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
            }
            this.viewer.entities.add(setting)
        })
            // 落点线
            ; (droppointPositions || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
                let setting = {
                    name: 'work-drone-route--planar-polyline',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
                }
                this.viewer.entities.add(setting)
            })
        // 面状航线第一个点突出展示
        isShowPointBillboard && this.startingIncreasePoint(positionArray)
@@ -340,9 +580,7 @@
            name: 'work-drone-route-planar-border',
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArrayHeights(
                    cloneCoordArr.flat(),
                ),
                positions: Cesium.Cartesian3.fromDegreesArrayHeights(cloneCoordArr.flat()),
                width: 4,
                material: new Cesium.PolylineOutlineMaterialProperty({
                    color: new Cesium.Color.fromBytes(74, 138, 233),
@@ -357,11 +595,8 @@
        // 绘制面状地块--------------------
        this.viewer.entities.add({
            name: 'work-drone-route-planar-polygon',
            polygon: {
                hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(
                    newCoordArr.flat(),
                ),
                hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(newCoordArr.flat()),
                material: new Cesium.Color.fromBytes(75, 159, 221, 100),
                outline: true,
                outlineColor: new Cesium.Color.fromBytes(35, 85, 216, 255),
@@ -384,7 +619,7 @@
                label: {
                    text: data.job_name || '',
                    font: 'bold 16px YouSheBiaoTiHei',
                    font: 'bold 16px sans-serif',
                    fillColor: Cesium.Color.WHITE,
                    pixelOffset: new Cesium.Cartesian2(0, -50),
                    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
@@ -395,64 +630,31 @@
            })
        } else {
            // 显示机巢位置
            if (isShowDock) {
                // 起点
                this.viewer.entities.add({
                    name: 'work-drone-route-point-dock',
                    position: droneTransformPosition,
                    billboard: {
                        image: endingOnlineImg,
                        width: 50,
                        height: 50,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    },
                    customData: {
                        data
                    }
                })
                this.viewer.entities.add({
                    name: 'work-drone-route--point-polyline',
                    position: droneTransformPosition,
                    polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0),
                })
            }
            const startPosition = Cesium.Cartesian3.fromDegrees(
                Number(filePositions[0].lng),
                Number(filePositions[0].lat),
                Number(filePositions[0].alt)
            )
            isShowDock && renderingMachineNest({
                viewer: this.viewer,
                dockTransformPosition,
                dockPosition,
                data
            })
            // 起点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-start',
                position: startPosition,
                billboard: {
                    image: rwqfdImg,
                    width: 50,
                    height: 50,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    data
                }
            isShowPointBillboard && renderingStartingPoint({
                viewer: this.viewer,
                startPosition: Cesium.Cartesian3.fromDegrees(
                    Number(filePositions[0].lng),
                    Number(filePositions[0].lat),
                    Number(filePositions[0].alt)
                ),
                data
            })
        }
        return {
            entity: planarRouteEntity,
            routeLinePositions: positionArray,
            filePositions: dronePosition != null ? [
                { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height },
                ...filePositions
            ] : filePositions
            filePositions:
                dockPosition != null
                    ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions]
                    : filePositions,
        }
    }
@@ -472,9 +674,7 @@
        let setting = {
            name: 'work-drone-route-planar-start',
            position: positions,
            billboard: {
                image: this.planarBillboard('#2D8CF0'),
                pixelOffset: new Cesium.Cartesian2(146, 72),
@@ -489,35 +689,48 @@
     * 绘制点航线
     * @param {*} lineObj 航线文件中的
     * @param {*} missionConfig 航线文件中的
     * @param {*} dronePosition 无人机位置
     * @param {*} dockPosition 无人机位置
     * @param {*} data 任务信息,航线地址等
     * @param {*} startPoint
     * @param {*} startPoint
     * @param {*} isShowDock 是否显示机巢位置
     * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注
     * @returns
     * @returns
     */
    async drawPointRoute (lineObj, missionConfig, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) {
        const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type)
        const droneTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dronePosition.longitude),
            Number(dronePosition.latitude),
            Number(dronePosition.height),
    async drawPointRoute (
        lineObj,
        missionConfig,
        dockPosition,
        data,
        startPoint,
        isShowDock,
        isShowPointBillboard,
        wayline_type
    ) {
        const { positionArray, filePositions } = this.disposeData(
            lineObj,
            missionConfig,
            dockPosition,
            data,
            startPoint,
            wayline_type
        )
        const dockTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dockPosition.longitude),
            Number(dockPosition.latitude),
            Number(dockPosition.height)
        )
        // 路径线
        let polyline
        indexLog = indexLog + 1
        if (this.type === 'clusterScheduling') {
            filePositions.forEach((item, index) => {
                let position = Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt)
                if (index === 0) {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-start',
                        position,
                        billboard: {
                            image: rwqfdImg,
                            outlineWidth: 0,
@@ -525,10 +738,9 @@
                            height: 50,
                            disableDepthTestDistance: Number.POSITIVE_INFINITY,
                        },
                        label: {
                            text: data.job_name || '',
                            font: 'bold 16px YouSheBiaoTiHei',
                            font: 'bold 16px sans-serif',
                            fillColor: data.type === 2 ? runningTextColor : pendingTextColor,
                            pixelOffset: data.type === 2 ? runningTextOffset : pendingTextOffset,
                            style: Cesium.LabelStyle.FILL_AND_OUTLINE,
@@ -540,9 +752,7 @@
                } else if (index === filePositions.length - 1) {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-end',
                        position,
                        billboard: {
                            image: newEndPointImg,
                            outlineWidth: 0,
@@ -556,9 +766,7 @@
                } else {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-approach',
                        position,
                        billboard: {
                            image: newNumPoint,
                            width: 50,
@@ -577,96 +785,56 @@
                    })
                }
            })
            polyline = this.viewer.entities.add({
                name: 'work-drone-route-point-polyline',
                polyline: {
                    width: 4,
                    positions: positionArray,
                    material: data.type === 2 ? runningLineMaterial : pendingLineMaterial,
                    material: data.type === 2 ? getLineColor(indexLog) : arrowLineMaterialPropertyGray,
                    clampToGround: false,
                }
                },
            })
        } else {
            // 显示机巢位置
            if (isShowDock) {
                // 起点
                this.viewer.entities.add({
                    name: 'work-drone-route-point-dock',
                    position: droneTransformPosition,
                    billboard: {
                        image: endingOnlineImg,
                        width: 50,
                        height: 50,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    },
                    customData: {
                        data
                    }
                })
                this.viewer.entities.add({
                    name: 'work-drone-route--point-polyline',
                    position: droneTransformPosition,
                    polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0),
                })
            }
            const startPosition = Cesium.Cartesian3.fromDegrees(
                Number(filePositions[0].lng),
                Number(filePositions[0].lat),
                Number(filePositions[0].alt)
            )
            isShowDock && renderingMachineNest({
                viewer: this.viewer,
                dockTransformPosition,
                dockPosition
            })
            // 起点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-start',
                position: startPosition,
                billboard: {
                    image: rwqfdImg,
                    width: 50,
                    height: 50,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    data
                }
            isShowPointBillboard && renderingStartingPoint({
                viewer: this.viewer,
                startPosition: Cesium.Cartesian3.fromDegrees(
                    Number(filePositions[0].lng),
                    Number(filePositions[0].lat),
                    Number(filePositions[0].alt)
                ),
                data
            })
            // 终点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-end',
                position: positionArray[positionArray.length - 1],
                billboard: {
                    image: new Cesium.ConstantProperty(endPointImg),
                    width: 30,
                    height: 30,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐
                },
            })
            isShowPointBillboard &&
                this.viewer.entities.add({
                    name: 'work-drone-route-point-end',
                    position: positionArray[positionArray.length - 1],
                    billboard: {
                        image: new Cesium.ConstantProperty(endPointImg),
                        width: 30,
                        height: 30,
                        verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐
                    },
                })
            console.log(data.status, '失败')
            polyline = this.viewer.entities.add({
                name: 'work-drone-route-point-polyline',
                polyline: {
                    width: 4,
                    positions: positionArray,
                    material: getLineColor(indexLog),
                    material: data.status === 5 ? arrowLineMaterialPropertyGray : getLineColor(indexLog),
                    clampToGround: false,
                },
                customData: {
                    data
                }
                    data,
                },
            })
        }
@@ -674,47 +842,45 @@
            longitude: Number(i.lng),
            latitude: Number(i.lat),
            height: Number(i.alt),
        }));
        }))
        // 落点线
        (droppointPositions || [])?.forEach((item, index) => {
            const topPosition = Cesium.Cartesian3.fromDegrees(
                Number(item.longitude),
                Number(item.latitude),
                Number(item.height)
            )
            // 落点线
            ; (droppointPositions || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
            let setting = {
                name: 'work-drone-route-point-polyline',
                position: topPosition,
                polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
            }
            this.viewer.entities.add(setting)
        })
                let setting = {
                    name: 'work-drone-route--point-polyline',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
                }
                this.viewer.entities.add(setting)
            })
        return {
            entity: polyline,
            routeLinePositions: positionArray,
            filePositions: dronePosition != null ? [
                { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height },
                ...filePositions
            ] : filePositions,
            filePositions:
                dockPosition !== null
                    ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions]
                    : filePositions,
        }
    }
    /**
     * 通用处理数据
     * @param {*} lineObj
     * @param {*} missionConfig
     * @param {*} dronePosition
     * @param {*} startPoint
     * @param {*} lineObj
     * @param {*} missionConfig
     * @param {*} dronePosition
     * @param {*} startPoint
     * @returns {} {positionArray: 带拼接点位置, filePositions:光航线点位置}
     */
    disposeData (lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) {
        indexLog = indexLog + 1
        const { device_sn } = data
        const executeHeightMode = lineObj.executeHeightMode === "WGS84"
        const executeHeightMode = lineObj.executeHeightMode === 'WGS84'
        let filePositions = lineObj.Placemark.map(item => {
            const [lng, lat] = item.Point.coordinates.split(',')
@@ -730,12 +896,14 @@
        let safetyHeight
        if (executeHeightMode) {
            let startHeight = new Decimal(missionConfig.takeOffSecurityHeight)
                .plus(dronePosition.height).toNumber()
            let startHeight = new Decimal(missionConfig.takeOffSecurityHeight).plus(dronePosition?.height || 0).toNumber()
            safetyHeight = startHeight < filePositions[0].alt ? filePositions[0].alt : startHeight
        } else {
            safetyHeight = missionConfig.takeOffSecurityHeight < filePositions[0].alt ? filePositions[0].alt : missionConfig.takeOffSecurityHeight
            safetyHeight =
                missionConfig.takeOffSecurityHeight < filePositions[0].alt
                    ? filePositions[0].alt
                    : missionConfig.takeOffSecurityHeight
        }
        let safetyPositions = []
@@ -773,28 +941,38 @@
        })
        this.updataMultiNestData(device_sn, {
            currentRoutePositions
            currentRoutePositions,
        })
        if ([0].includes(wayline_type)) {
            let startHeight = executeHeightMode ? filePositions[0].alt : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode ? filePositions[1].alt : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let startHeight = executeHeightMode
                ? filePositions[0].alt
                : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode
                ? filePositions[1].alt
                : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            this.updataMultiNestData(device_sn, {
                showPopupPosition: Cesium.Cartesian3.midpoint(
                    Cesium.Cartesian3.fromDegrees(filePositions[0].lng, filePositions[0].lat, startHeight),
                    Cesium.Cartesian3.fromDegrees(filePositions[1].lng, filePositions[1].lat, endHeight),
                    new Cesium.Cartesian3())
                    new Cesium.Cartesian3()
                ),
            })
        } else {
            let startHeight = executeHeightMode ? safetyPositions[1].alt : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode ? safetyPositions[2].alt : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0)
            let startHeight = executeHeightMode
                ? safetyPositions[1].alt
                : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode
                ? safetyPositions[2].alt
                : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0)
            this.updataMultiNestData(device_sn, {
                showPopupPosition: Cesium.Cartesian3.midpoint(
                    Cesium.Cartesian3.fromDegrees(safetyPositions[1].lng, safetyPositions[1].lat, startHeight),
                    Cesium.Cartesian3.fromDegrees(safetyPositions[2].lng, safetyPositions[2].lat, endHeight),
                    new Cesium.Cartesian3())
                    new Cesium.Cartesian3()
                ),
            })
        }
@@ -805,9 +983,9 @@
                return {
                    ...item,
                    alt: Number(height)
                    alt: Number(height),
                }
            })
            }),
        }
    }
@@ -819,9 +997,10 @@
            return i?.name && i?.name.includes('work-drone-route-')
        })
        Array.isArray(entities) && entities.forEach(item => {
            this.viewer?.entities.remove(item)
        })
        Array.isArray(entities) &&
            entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
    }
    // 当前无人机视椎体相关
@@ -847,11 +1026,22 @@
                ['wide', 'ir', 'zoom'].includes(curDroneData?.camera_type)
            ) {
                if (curDroneData.camera_type === 'wide') {
                    fov = 60
                    fov = 65
                } else {
                    const diffFocal = zoomFactor.value[curDroneData.camera_type].max - getZoomFactor(host?.cameras?.[0], curDroneData.camera_type)
                    fov = (60 / zoomFactor.value[curDroneData.camera_type].max) * (diffFocal === 0 ? 1 : diffFocal)
                    const cameraMultiplier = getZoomFactor(host?.cameras?.[0], curDroneData.camera_type)
                    switch (cameraMultiplier) {
                        case 2: fov = 30; break
                        case 3: fov = 19; break
                        case 4: fov = 15; break
                        case 5: fov = 13; break
                        case 6: fov = 11; break
                        case 7: fov = 9; break
                        case 8: fov = 8.34; break
                        case 9: fov = 7.68; break
                        case 10: fov = 6.9; break
                        case 14: fov = 5; break
                        default: fov = 55.946 * Math.exp(-0.4504 * cameraMultiplier) + 6.323
                    }
                }
            }
@@ -871,8 +1061,8 @@
                    pitch: 0,
                    heading: attitude_head,
                    isShowVideoPlan: this.isShowVideoPlan
                })
                    isShowVideoPlan: this.isShowVideoPlan,
                }),
            })
        }
    }
@@ -882,7 +1072,7 @@
        if (device_sn && live_status) {
            this.updataMultiNestData(device_sn, {
                camera_type: live_status?.[0].video_type || 'wide'
                camera_type: live_status?.[0].video_type || 'wide',
            })
        }
    }
@@ -897,14 +1087,9 @@
        }
    }
    // 设置当前无人机视椎体信息面板
    setFrustumInfoDetails (data) {
        const {
            dock_sn,
            distance_to_airport,
            distance_to_next_point,
            estimated_time_to_next_point,
            plane_mileage
        } = data
        const { dock_sn, distance_to_airport, distance_to_next_point, estimated_time_to_next_point, plane_mileage } = data
        if (dock_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
@@ -924,7 +1109,9 @@
                }
                aircraftEntity.label = new Cesium.LabelGraphics({
                    text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(distance_to_next_point)}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`,
                    text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(
                        distance_to_next_point
                    )}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`,
                    font: '13px monospace',
                    showBackground: true,
                    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
@@ -936,7 +1123,6 @@
                return
            }
        }
    }
@@ -954,8 +1140,10 @@
        }
    }
    // 设置无人机模型
    setAircraftGltf (host) {
        const parent_sn = host?.parent_sn
        const attitude_head = Number(host?.payloads?.[0]?.gimbal_yaw) - 90 || 0
        if (parent_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
@@ -963,9 +1151,18 @@
            })
            const position = Cesium.Cartesian3.fromDegrees(host?.longitude, host?.latitude, host?.height)
            // ✅ 新增:Heading / Pitch / Roll(单位:度 -> 弧度)
            const heading = Cesium.Math.toRadians(attitude_head) // 航向角
            const pitch = Cesium.Math.toRadians(0)     // 俯仰角
            const roll = Cesium.Math.toRadians(0)       // 横滚角
            // ✅ 新增:构造 HeadingPitchRoll 和 四元数
            const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll)
            const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)
            if (aircraftEntity) {
                aircraftEntity.position = new Cesium.ConstantPositionProperty(position)
                aircraftEntity.orientation = new Cesium.ConstantProperty(orientation)
                return
            }
@@ -974,18 +1171,35 @@
                this.viewer?.entities.add({
                    name: `aircraft-glf-${parent_sn}`,
                    position,
                    orientation,
                    label: {},
                    model: {
                        uri: aircraftGltf, // 或 .glb
                        scale: 1.0, // 缩放比例
                        scale: 64, // 缩放比例
                        minimumPixelSize: 64, // 最小像素尺寸(保证模型远处可见)
                        maximumScale: 128, // 最大缩放(可选)
                        maximumScale: 64, // 最大缩放(可选)
                    },
                })
            }
        }
    }
    // 移除无人机模型
    removeAircraftGltf (host) {
        const parent_sn = host?.parent_sn
        if (parent_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
                return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`)
            })
            if (aircraftEntity) {
                this.viewer?.entities.remove(aircraftEntity)
            }
        }
    }
    // 移除所有无人机视椎体
    removeViewInfoFrustum () {
        this.multiNestData.forEach(item => {
            if ('viewInfoFrustum' in item && item.viewInfoFrustum) {
@@ -1010,9 +1224,10 @@
                return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`)
            })
            Array.isArray(entities) && entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
            Array.isArray(entities) &&
                entities.forEach(item => {
                    this.viewer?.entities.remove(item)
                })
        }
    }
@@ -1021,13 +1236,15 @@
            return i?.name && i?.name.includes(`aircraft-glf-`)
        })
        Array.isArray(entities) && entities.forEach(item => {
            this.viewer?.entities.remove(item)
        })
        Array.isArray(entities) &&
            entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
        this.multiNestData.length > 0 && (this.multiNestData.forEach(item => {
            item.viewInfoFrustum?.clear()
        }))
        this.multiNestData.length > 0 &&
            this.multiNestData.forEach(item => {
                item.viewInfoFrustum?.clear()
            })
    }
    /**
@@ -1043,19 +1260,19 @@
            this.multiNestData[isHaveInd] = {
                ...this.multiNestData[isHaveInd],
                ...data
                ...data,
            }
        } else {
            this.multiNestData.push({
                device_sn,
                ...data
                ...data,
            })
        }
    }
    /**
     * 根据机巢sn获取当前机巢对应的相关信息
     * @param {*} device_sn
     * @param {*} device_sn
     */
    getCurDroneData (device_sn) {
        const isHave = this.multiNestData.find(item => item.device_sn === device_sn)
@@ -1071,14 +1288,16 @@
            return {
                ...item,
                show: true,
                uuid: uuidv4()
                uuid: uuidv4(),
            }
        })
        this.viewer.scene.postRender.addEventListener(that.labelBoxRenderHandler)
    }
    showSingleDetailMapPopup (data) {
        this.detailsMapPopupData.forEach(i => i.job_id === data.job_id && i.device_sn === data.device_sn && (i.show = true))
        this.detailsMapPopupData.forEach(
            i => i?.job_id === data?.job_id && i?.device_sn === data?.device_sn && (i.show = true)
        )
    }
    getLabelDom (data) {
@@ -1090,28 +1309,30 @@
        tooltipContainer.style.position = 'absolute'
        tooltipContainer.style.transform = 'translate(-50%,10%)'
        tooltipContainer.style.pointerEvents = 'none'
        document.querySelector('.content-map-popups')?.append(tooltipContainer)
        document.querySelector('.content-map-popups').append(tooltipContainer)
        render(vNode, tooltipContainer)
        return tooltipContainer
    }
    labelBoxRender () {
        this.detailsMapPopupData.filter(i => i.show === true).forEach(item => {
            const { showPopupPosition } = this.getCurDroneData(item.device_sn)
        this.detailsMapPopupData
            .filter(i => i.show === true)
            .forEach(item => {
                const { showPopupPosition } = this.getCurDroneData(item.device_sn)
            let dom = document.querySelector(`[id="${item.uuid}"]`)
                let dom = document.querySelector(`[id="${item.uuid}"]`)
            if (!dom) {
                dom = this.getLabelDom(item)
            }
                if (!dom) {
                    dom = this.getLabelDom(item)
                }
            const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition)
            if (screenPosition) {
                dom.style.left = `${screenPosition.x}px`
                dom.style.top = `${screenPosition.y}px`
                dom.style.display = 'block'
            }
        })
                const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition)
                if (screenPosition) {
                    dom.style.left = `${screenPosition.x}px`
                    dom.style.top = `${screenPosition.y}px`
                    dom.style.display = 'block'
                }
            })
    }
    // 移除弹框标签
@@ -1137,4 +1358,4 @@
        const that = this
        that.viewer?.scene.postRender.removeEventListener(that.labelBoxRenderHandler)
    }
}
}
applications/task-work-order/src/utils/cesium/eventBus.js
File was deleted
applications/task-work-order/src/utils/cesium/frustum/CesiumVideoFrustum.js
New file
@@ -0,0 +1,712 @@
/*
 * @Author       : yuan
 * @Date         : 2025-08-16 17:01:46
 * @LastEditors  : yuan
 * @LastEditTime : 2025-09-26 16:27:20
 * @FilePath     : \src\utils\cesium\frustum\CesiumVideoFrustum.js
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-08-16 17:01:46
 */
import * as Cesium from 'cesium'
var videoShed3dShader = `
uniform float ztzf_frustum_opacity;
uniform sampler2D ztzf_frustum_videoTexture;
uniform sampler2D ztzf_frustum_maskTexture;
uniform vec4 ztzf_frustum_hiddenAreaColor;
uniform sampler2D shadowMap_texture;
uniform mat4 shadowMap_matrix;
uniform vec4 shadowMap_lightPositionEC;
uniform vec4 shadowMap_texelSizeDepthBias;
uniform vec4 shadowMap_normalOffsetScale;
uniform bool ztzf_frustum_flipx;
uniform bool ztzf_frustum_flipy;
// 新增边框参数
uniform vec4 ztzf_frustum_borderColor;  // 边框颜色(RGBA)
uniform float ztzf_frustum_borderWidth; // 边框宽度(0.0-1.0)
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
in vec2 v_textureCoordinates;
vec4 toEye(in vec2 uv, in float depth) {
  vec2 xy = vec2((uv.x * 2.0 - 1.0), (uv.y * 2.0 - 1.0));
  vec4 posInCamera = czm_inverseProjection * vec4(xy, depth, 1.0);
  posInCamera = posInCamera / posInCamera.w;
  return posInCamera;
}
float getDepthMars3D(in vec4 depth) {
  float z_window = czm_unpackDepth(depth);
  z_window = czm_reverseLogDepth(z_window);
  float n_range = czm_depthRange.near;
  float f_range = czm_depthRange.far;
  return (2.0 * z_window - n_range - f_range) / (f_range - n_range);
}
float _czm_sampleShadowMap(sampler2D shadowMap, vec2 uv) {
  return texture(shadowMap, uv).r;
}
float _czm_shadowDepthCompare(sampler2D shadowMap, vec2 uv, float depth) {
  return step(depth, _czm_sampleShadowMap(shadowMap, uv));
}
float _czm_shadowVisibility(sampler2D shadowMap, czm_shadowParameters shadowParameters) {
  float depthBias = shadowParameters.depthBias;
  float depth = shadowParameters.depth;
  float nDotL = shadowParameters.nDotL;
  float normalShadingSmooth = shadowParameters.normalShadingSmooth;
  float darkness = shadowParameters.darkness;
  vec2 uv = shadowParameters.texCoords;
  depth -= depthBias;
  vec2 texelStepSize = shadowParameters.texelStepSize;
  float radius = 1.0;
  float dx0 = -texelStepSize.x * radius;
  float dy0 = -texelStepSize.y * radius;
  float dx1 = texelStepSize.x * radius;
  float dy1 = texelStepSize.y * radius;
  float visibility = (_czm_shadowDepthCompare(shadowMap, uv, depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, 0.0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, 0.0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy1), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy1), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy1), depth)) * (1.0 / 9.0);
  return visibility;
}
vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point) {
  vec3 v01 = point - planeOrigin;
  float d = dot(planeNormal, v01);
  return (point - planeNormal * d);
}
float ptm(vec3 pt) {
  return sqrt(pt.x * pt.x + pt.y * pt.y + pt.z * pt.z);
}
void main() {
  const float PI = 3.141592653589793;
  vec4 color = texture(colorTexture, v_textureCoordinates);
  vec4 currD = texture(depthTexture, v_textureCoordinates);
  float depth = getDepthMars3D(currD);
  vec4 positionEC = toEye(v_textureCoordinates, depth);
  vec3 normalEC = vec3(1.0);
  czm_shadowParameters shadowParameters;
  shadowParameters.texelStepSize = shadowMap_texelSizeDepthBias.xy;
  shadowParameters.depthBias = shadowMap_texelSizeDepthBias.z;
  shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBias.w;
  shadowParameters.darkness = shadowMap_normalOffsetScale.w;
  shadowParameters.depthBias *= max(depth * 0.01, 1.0);
  vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz);
  float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0);
  vec4 shadowPosition = shadowMap_matrix * positionEC;
  shadowPosition /= shadowPosition.w;
  if(any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) {
    out_FragColor = color;
    return;
  }
  shadowParameters.texCoords = shadowPosition.xy;
  shadowParameters.depth = shadowPosition.z;
  shadowParameters.nDotL = nDotL;
  float visibility = _czm_shadowVisibility(shadowMap_texture, shadowParameters);
  //视频投射
  if(visibility == 1.0) {
    if(ztzf_frustum_flipx){
      shadowPosition.x = shadowPosition.x + (0.5 - shadowPosition.x) * 2.0;
    }
    if(ztzf_frustum_flipy){
      shadowPosition.y = shadowPosition.y + (0.5 - shadowPosition.y) * 2.0;
    }
    vec4 videoColor = texture(ztzf_frustum_videoTexture, shadowPosition.xy);
    vec4 maskColor = texture(ztzf_frustum_maskTexture, shadowPosition.xy);
    videoColor *= maskColor;
    // 计算到边缘的距离
    float distToEdge = min(
        min(shadowPosition.x, 1.0 - shadowPosition.x),
        min(shadowPosition.y, 1.0 - shadowPosition.y)
    );
    // 混合视频颜色和背景
    vec4 finalColor = mix(color, vec4(videoColor.xyz, 1.0), ztzf_frustum_opacity * videoColor.a);
    // 添加边框效果
    if(distToEdge < ztzf_frustum_borderWidth && videoColor.a > 0.0) {
        float borderFactor = smoothstep(0.0, ztzf_frustum_borderWidth, distToEdge);
        finalColor = mix(ztzf_frustum_borderColor, finalColor, borderFactor);
    }
    out_FragColor = finalColor;
  } else {
    if(abs(shadowPosition.z - 0.0) < 0.01) {
      return;
    }
    out_FragColor = vec4(mix(color.rgb, ztzf_frustum_hiddenAreaColor.rgb, ztzf_frustum_hiddenAreaColor.a), ztzf_frustum_hiddenAreaColor.a);
  }
}
    `
class CesiumVideoFrustum {
    constructor(viewer, param) {
        this.param = param
        var option = this._initCameraParam()
        if (option || (option = {}), this.viewer = viewer, this._cameraPosition = option.cameraPosition, this._position = option.position,
            this.type = option.type, this._alpha = option.alpha || 0, this.element = option.element, this.color = option.color,
            this._debugFrustum = Cesium.defaultValue(option.debugFrustum, !0), this._aspectRatio = option.aspectRatio || this._getWinWidHei(),
            this._camerafov = option.fov || Cesium.Math.toDegrees(this.viewer.scene.camera.frustum.fov),
            this._isShadowMap = option.isShadowMap || false,
            this.texture = option.texture || new Cesium.Texture({
                context: this.viewer.scene.context,
                source: {
                    width: 1,
                    height: 1,
                    arrayBufferView: new Uint8Array([255, 255, 255, 255])
                },
                flipY: !1
            }), this.defaultShow = Cesium.defaultValue(option.show, !0), !this.cameraPosition || !this.position) return void console.log('初始化失败:请确认相机位置与视点位置正确!')
        this.activeVideo()
        this._createShadowMap()
        this._getOrientation()
        this._addCameraFrustum()
        this._addPostProcess()
        this.viewer.scene.primitives.add(this)
    }
    get alpha () {
        return this._alpha
    }
    set alpha (e) {
        this._alpha = e
        this._changeViewPos()
    }
    get aspectRatio () {
        return this._aspectRatio
    }
    set aspectRatio (e) {
        this._aspectRatio = e
        this._changeVideoWidHei()
    }
    get debugFrustum () {
        return this._debugFrustum
    }
    set debugFrustum (e) {
        this._debugFrustum = e
        this.cameraFrustum.show = e
        this.cameraFrustumOutline.show = e
    }
    get fov () {
        return this._camerafov
    }
    set fov (e) {
        this._camerafov = e
        this._changeCameraFov()
    }
    get cameraPosition () {
        return this._cameraPosition
    }
    set cameraPosition (e) {
        e && (this._cameraPosition = e, this._changeCameraPos())
    }
    get isShadowMap () {
        return this._isShadowMap
    }
    set isShadowMap (e) {
        this._isShadowMap = e
        this._changeCameraIsShadowMap()
    }
    get position () {
        return this._position
    }
    set position (e) {
        e && (this._position = e, this._changeViewPos())
    }
    get params () {
        var t = {}
        return t.element = this.element,
            t.position = this.position,
            t.cameraPosition = this.cameraPosition,
            t.fov = this.fov,
            t.isShadowMap = this.isShadowMap,
            t.aspectRatio = this.aspectRatio,
            t.alpha = this.alpha,
            t.debugFrustum = this.debugFrustum,
            t.show = this.show
    }
    get show () {
        return this.defaultShow
    }
    set show (e) {
        this.defaultShow = Boolean(e),
            this._switchShow()
    }
    isDestroyed () {
        return false
    }
    _initCameraParam () {
        var cameraPosition = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1)
        const hpr = Cesium.HeadingPitchRoll.fromDegrees(this.param.rotation.y * 1, this.param.rotation.x * 1, 0)
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(cameraPosition, hpr)
        let matrix = Cesium.Matrix3.fromQuaternion(
            orientation,
            new Cesium.Matrix3()
        )
        let direction = Cesium.Matrix3.getColumn(matrix, 2, new Cesium.Cartesian3())
        let frontDirect = Cesium.Cartesian3.multiplyByScalar(
            direction,
            250,
            new Cesium.Cartesian3()
        )
        let position = Cesium.Cartesian3.add(cameraPosition, frontDirect, new Cesium.Cartesian3())
        return {
            cameraPosition: cameraPosition,
            position: position,
            alpha: this.param.alpha,
            near: this.param.near,
            fov: this.param.fov,
            isShadowMap: this.param.isShadowMap,
            debugFrustum: this.param.debugFrustum,
            aspectRatio: this.param.aspectRatio,
            element: this.param.element,
        }
    }
    _changeAlpha (e) {
        this.alpha = e
    }
    _changeAspectRatio (e) {
        this.aspectRatio = e
    }
    _changeRotation (e) {
        if (e) {
            this.param.rotation = e
            var option = this._initCameraParam()
            this.position = option.position
        }
    }
    _changeCameraPosition (e) {
        if (e) {
            this.param.position = e
            var option = this._initCameraParam()
            this.cameraPosition = option.cameraPosition
        }
    }
    _changeFov (e) {
        if (e) {
            this.param.fov = e
            var option = this._initCameraParam()
            this.fov = option.fov
        }
    }
    _changeIsShadowMap (e) {
        this.param.isShadowMap = e
        var option = this._initCameraParam()
        this.isShadowMap = option.isShadowMap
    }
    _changeFar (e) {
        if (e) {
            this.param.far = e
            var option = this._initCameraParam()
            this.position = option.position
        }
    }
    _changeNear (e) {
        if (e) {
            this.param.near = e
            this.near = this.param.near
            this._changeCameraPos()
        }
    }
    _getWinWidHei () {
        var viewer = this.viewer.scene
        return viewer.canvas.clientWidth / viewer.canvas.clientHeight
    }
    _changeCameraFov () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeCameraIsShadowMap () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeVideoWidHei () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeCameraPos () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            // this.cameraFrustum.destroy(),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeViewPos () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            // this.cameraFrustum.destroy(),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _switchShow () {
        this.show ? !this.postProcess && this._addPostProcess() : (this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess), delete this.postProcess, this.postProcess = null),
            this.cameraFrustum.show = this.show,
            this.cameraFrustumOutline.show = this.show
    }
    activeVideo () {
        var video = this.element,
            that = this
        if (video) {
            var viewer = this.viewer
            this.activeVideoListener || (this.activeVideoListener = function () {
                that.videoTexture && that.videoTexture.destroy(),
                    that.videoTexture = new Cesium.Texture({
                        context: viewer.scene.context,
                        source: video,
                        width: 1,
                        height: 1,
                        pixelFormat: Cesium.PixelFormat.RGBA,
                        pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE
                    })
            }),
                viewer.clock.onTick.addEventListener(this.activeVideoListener)
        }
    }
    locate () {
        var cameraPosition = Cesium.clone(this.cameraPosition),
            position = Cesium.clone(this.position)
        this.viewer.Camera.position = cameraPosition,
            this.viewer.camera.direction = Cesium.Cartesian3.subtract(position, cameraPosition, new Cesium.Cartesian3(0, 0, 0)),
            this.viewer.camera.up = Cesium.Cartesian3.normalize(cameraPosition, new Cesium.Cartesian3(0, 0, 0))
    }
    update (e) {
        this.viewShadowMap && this.viewer.scene.frameState.shadowMaps.push(this.viewShadowMap) // *重点* 多投影
    }
    _createShadowMap () {
        var e = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1)
        var t = this.position
        var i = this.viewer.scene
        var a = new Cesium.Camera(i)
        a.position = e
        this.customFrustum = new Cesium.PerspectiveFrustum({
            fov: Cesium.Math.toRadians(this.fov),
            aspectRatio: this.aspectRatio,
            near: this.near,
            far: 250
        })
        a.frustum = new Cesium.PerspectiveFrustum({
            fov: Cesium.Math.toRadians(this.fov),
            aspectRatio: this.aspectRatio,
            near: this.near,
            far: 250
        })
        let curOrientation = {}
        const headingPitchRoll = new Cesium.HeadingPitchRoll(
            Cesium.Math.toRadians(this.param.rotation.y, 0),
            Cesium.Math.toRadians(this.param.rotation.x, 0),
            Cesium.Math.toRadians(0),
        )
        curOrientation['heading'] = headingPitchRoll['heading']
        curOrientation['pitch'] = headingPitchRoll['pitch']
        curOrientation['roll'] = headingPitchRoll['roll']
        let curDestination = {}
        curDestination.destination = e
        curDestination.orientation = curOrientation
        a.setView(curDestination)
        var n = Cesium.Cartesian3.distance(t, e)
        this.viewDis = n
        if (!this.isShadowMap) return
        this.viewShadowMap = new Cesium.ShadowMap({
            lightCamera: a,
            enable: !1,
            isPointLight: !1,
            isSpotLight: !0,
            cascadesEnabled: !1,
            context: i.context,
            pointLightRadius: n,
        })
    }
    _getOrientation () {
        const hpr = Cesium.HeadingPitchRoll.fromDegrees(180 + this.param.rotation.y * 1, 0, 90 - this.param.rotation.x * 1 || 0)
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(this.cameraPosition, hpr)
        return this.orientation = orientation,
            orientation
    }
    _addCameraFrustum () {
        var e = this
        this.cameraFrustum = new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.FrustumGeometry({
                    origin: e.cameraPosition,
                    orientation: e.orientation,
                    frustum: e.customFrustum,
                    _drawNearPlane: !0
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromBytes(0, 213, 144, 20)
                    ),
                }
            }),
            releaseGeometryInstances: false,
            appearance: new Cesium.PerInstanceColorAppearance({
                translucent: true,
                flat: !0,
                closed: true,
            }),
            asynchronous: false,
            show: this.debugFrustum && this.show
        })
        this.cameraFrustumOutline = new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.FrustumOutlineGeometry({
                    origin: e.cameraPosition,
                    orientation: e.orientation,
                    frustum: e.customFrustum,
                    _drawNearPlane: !0
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromBytes(0, 213, 144, 255)
                    ),
                }
            }),
            appearance: new Cesium.PerInstanceColorAppearance({
                closed: true,
                flat: true,
                translucent: true
            }),
            asynchronous: false,
            show: this.debugFrustum && this.show
        })
        this.viewer.scene.primitives.add(this.cameraFrustum)
        this.viewer.scene.primitives.add(this.cameraFrustumOutline)
    }
    _addPostProcess () {
        if (!this.isShadowMap) return
        var e = this,
            t = videoShed3dShader
        const i = e.viewShadowMap['_primitiveBias']
        this.postProcess = new Cesium.PostProcessStage({
            fragmentShader: t,
            uniforms: {
                ztzf_frustum_videoTexture: function () {
                    return e.videoTexture
                },
                ztzf_frustum_maskTexture: function () {
                    return new Cesium['Texture']({
                        'context': e.viewer.scene.context,
                        'source': {
                            'width': 0x1,
                            'height': 0x1,
                            'arrayBufferView': new Uint8Array([255, 255, 255, 255])
                        },
                        'flipY': ![]
                    })
                },
                ztzf_frustum_opacity: function () {
                    return e.alpha
                },
                ztzf_frustum_hiddenAreaColor: () => {
                    return new Cesium.Color(0, 0, 0, 0.5)
                },
                shadowMap_texture: function () {
                    if (!e.viewShadowMap || !e.viewShadowMap['_shadowMapTexture'] || e.viewShadowMap['_shadowMapTexture']['isDestroyed']()) {
                        return new Cesium['Texture']({
                            'context': e.viewer.scene.context,
                            'source': {
                                'width': 0x1,
                                'height': 0x1,
                                'arrayBufferView': new Uint8Array([0x0, 0x0, 0x0, 0x0])
                            },
                            'flipY': ![]
                        })
                    }
                    return e.viewShadowMap._shadowMapTexture
                },
                shadowMap_matrix: function () {
                    return e.viewShadowMap._shadowMapMatrix
                },
                shadowMap_lightPositionEC: function () {
                    return e.viewShadowMap._lightPositionEC
                },
                shadowMap_texelSizeDepthBias: function () {
                    var t = new Cesium.Cartesian2
                    return t.x = 1 / e.viewShadowMap._textureSize.x,
                        t.y = 1 / e.viewShadowMap._textureSize.y,
                        Cesium.Cartesian4.fromElements(t.x, t.y, i.depthBias, i.normalShadingSmooth, this.combinedUniforms1)
                },
                shadowMap_normalOffsetScale: function () {
                    return Cesium.Cartesian4.fromElements(i.normalOffsetScale, e.viewShadowMap._distance, e.viewShadowMap.maximumDistance, e.viewShadowMap._darkness, this.combinedUniforms2)
                },
                ztzf_frustum_flipx: () => {
                    return false
                },
                ztzf_frustum_flipy: () => {
                    return false
                },
                ztzf_frustum_borderColor: () => {
                    return new Cesium.Color(0 / 255, 213 / 255, 144 / 255, 255 / 255)
                },
                ztzf_frustum_borderWidth: () => {
                    return 0.01
                },
            }
        }),
            this.viewer.scene.postProcessStages.add(this.postProcess)
    }
    destroy () {
        this.viewer.scene.postProcessStages.remove(this.postProcess),
            this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            //this._videoEle && this._videoEle.parentNode.removeChild(this._videoEle),
            this.activeVideoListener && this.viewer.clock.onTick.removeEventListener(this.activeVideoListener),
            this.activeVideoListener && delete this.activeVideoListener,
            delete this.postProcess,
            delete this.viewShadowMap,
            delete this.color,
            delete this.viewDis,
            delete this.cameraPosition,
            delete this.position,
            delete this.alpha,
            delete this._camerafov,
            delete this._isShadowMap,
            delete this._cameraPosition,
            delete this.videoTexture,
            delete this.cameraFrustum,
            delete this.cameraFrustumOutline,
            delete this._videoEle,
            delete this._debugFrustum,
            delete this._position,
            delete this._aspectRatio,
            delete this.element,
            delete this.orientation,
            delete this.texture,
            delete this.videoId,
            this.viewer.scene.primitives.remove(this),
            delete this.viewer
    }
}
export default CesiumVideoFrustum
applications/task-work-order/src/utils/cesium/frustum/CoordinateTranslate.js
New file
@@ -0,0 +1,229 @@
/*
 * @Author       : yuan
 * @Date         : 2025-08-15 14:06:49
 * @LastEditors  : yuan
 * @LastEditTime : 2025-08-15 14:06:59
 * @FilePath     : \src\utils\cesium\frustum\CoordinateTranslate.js
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-08-15 14:06:49
 */
let ECEF = (function () {
    var _ = function () {
        this.PI = 3.141592653589793238
        this.a = 6378137.0
        this.b = 6356752.3142
        this.f = (this.a - this.b) / this.a
        this.e_sq = this.f * (2.0 - this.f)
        this.ee = 0.00669437999013
        this.WGSF = 1 / 298.257223563
        this.WGSe2 = this.WGSF * (2 - this.WGSF)
        this.WGSa = 6378137.00000
        this.EPSILON = 1.0e-12
    }
    _.prototype.CalculateCoordinates = function (point, azimuth, elevation, distance) {
        var vertical_height = distance * Math.sin(2 * this.PI / 360 * elevation)//垂直高度
        var horizontal_distance = distance * Math.cos(2 * this.PI / 360 * elevation)//水平距离
        if (azimuth > 360) azimuth = azimuth % 360
        if (azimuth < 0) azimuth = 360 + (azimuth % 360)
        var point1 = this.lonLat2WebMercator(point)
        var lnglat = null
        var x_length, y_length
        if (azimuth <= 90) {//第四象限
            x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * azimuth)
            y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * azimuth)
            lnglat = {
                x: point1.x + x_length,
                y: point1.y - y_length
            }
        } else if (azimuth > 90 && azimuth <= 180) {//第三象限
            x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 90))
            y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 90))
            lnglat = {
                x: point1.x - x_length,
                y: point1.y - y_length
            }
        } else if (azimuth > 180 && azimuth <= 270) {//第二象限
            x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 180))
            y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 180))
            lnglat = {
                x: point1.x - x_length,
                y: point1.y + y_length
            }
        } else {//第一象限
            x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 270))
            y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 270))
            lnglat = {
                x: point1.x + x_length,
                y: point1.y + y_length
            }
        }
        lnglat = this.webMercator2LonLat(lnglat)
        return {
            lng: lnglat.x,
            lat: lnglat.y,
            height: vertical_height
        }
    }
    /*
       *经纬度转Web墨卡托
       *@lonLat 经纬度
       */
    _.prototype.lonLat2WebMercator = function (lonLat) {
        let x = lonLat.x * this.a / 180
        let y = Math.log(Math.tan((90 + lonLat.y) * this.PI / 360)) / (this.PI / 180)
        y = y * this.a / 180
        return {
            x: x,
            y: y
        }
    }
    /*
       *Web墨卡托转经纬度
       *@mercator 平面坐标
       */
    _.prototype.webMercator2LonLat = function (mercator) {
        let x = mercator.x / this.a * 180
        let y = mercator.y / this.a * 180
        y = 180 / this.PI * (2 * (Math.exp(y * this.PI / 180)) - this.PI / 2)
        return {
            x: x,
            y: y
        }
    }
    _.prototype.get_atan = function (z, y) {
        let x
        if (z == 0) {
            x = this.PI / 2
        } else {
            if (y == 0) {
                x = this.PI
            } else {
                x = Math.atan(Math.abs(y / z))
                if ((y > 0) && (z < 0)) {
                    x = this.PI - x
                } else if ((y < 0) && (z < 0)) {
                    x = this.PI + x
                } else if ((y < 0) && (z > 0)) {
                    x = 2 * this.M_PI - x
                }
            }
        }
        return x
    }
    //WGS84转ECEF坐标系
    _.prototype.ConvertLLAToXYZ = function (LLACoor) {
        let lon = this.PI / 180 * LLACoor.longitude
        let lat = this.PI / 180 * LLACoor.latitude
        let H = LLACoor.altitude
        let N0 = this.a / Math.sqrt(1.0 - this.ee * Math.sin(lat) * Math.sin(lat))
        let x = (N0 + H) * Math.cos(lat) * Math.cos(lon)
        let y = (N0 + H) * Math.cos(lat) * Math.sin(lon)
        let z = (N0 * (1.0 - this.ee) + H) * Math.sin(lat)
        return {
            x: x,
            y: y,
            z: z
        }
    }
    //ECEF坐标系转WGS84
    _.prototype.ConvertXYZToLLA = function (XYZCoor) {
        let longitude = this.get_atan(XYZCoor.x, XYZCoor.y)
        if (longitude < 0) {
            longitude = longitude + this.PI
        }
        let latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), XYZCoor.z)
        let W = Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude))
        let N = this.WGSa / W
        let B1
        do {
            B1 = latitude
            W = Math.sqrt(1 - this.WGSe2 * Math.sin(B1) * Math.sin(B1))
            N = this.WGSa / W
            latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), (XYZCoor.z + N * this.WGSe2 * Math.sin(B1)))
        }
        while (Math.abs(latitude - B1) > this.EPSILON)
        var altitude = Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y) / Math.cos(latitude) - this.WGSa / Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude))
        return {
            longitude: longitude * 180 / this.PI,
            latitude: latitude * 180 / this.PI,
            altitude: altitude
        }
    }
    /*北东天坐标系转WGS84
    @ a A点坐标
    @ p 相对参数,距离、方位角、仰角
    */
    //    俯视角pitch -elevation
    //航向角heading(yaw) -azimuth
    _.prototype.enu_to_ecef = function (a, p) {
        //距离
        let distance = p.distance
        //方位角
        let azimuth = p.azimuth
        //仰角
        let elevation = p.elevation
        let zUp = elevation >= 0 ? distance * Math.sin(this.PI / 180 * elevation) : (-1) * distance * Math.sin(this.PI / 180 * Math.abs(elevation))
        let d = distance * Math.cos(this.PI / 180 * Math.abs(elevation))
        let xEast
        let yNorth
        if (azimuth <= 90) {
            xEast = d * Math.sin(this.PI / 180 * azimuth)
            yNorth = d * Math.cos(this.PI / 180 * azimuth)
        } else if (azimuth > 90 && azimuth < 180) {
            xEast = d * Math.cos(this.PI / 180 * (azimuth - 90))
            yNorth = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 90))
        } else if (azimuth > 180 && azimuth < 270) {
            xEast = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 180))
            yNorth = (-1) * d * Math.cos(this.PI / 180 * (azimuth - 180))
        } else {
            xEast = (-1) * d * Math.sin(this.PI / 180 * (360 - azimuth))
            yNorth = d * Math.cos(this.PI / 180 * (360 - azimuth))
        }
        let lamb = this.radians(a.latitude)
        let phi = this.radians(a.longitude)
        let h0 = a.altitude
        let s = Math.sin(lamb)
        let N = this.a / Math.sqrt(1.0 - this.e_sq * s * s)
        let sin_lambda = Math.sin(lamb)
        let cos_lambda = Math.cos(lamb)
        let sin_phi = Math.sin(phi)
        let cos_phi = Math.cos(phi)
        let x0 = (h0 + N) * cos_lambda * cos_phi
        let y0 = (h0 + N) * cos_lambda * sin_phi
        let z0 = (h0 + (1 - this.e_sq) * N) * sin_lambda
        let t = cos_lambda * zUp - sin_lambda * yNorth
        let zd = sin_lambda * zUp + cos_lambda * yNorth
        let xd = cos_phi * t - sin_phi * xEast
        let yd = sin_phi * t + cos_phi * xEast
        return this.ConvertXYZToLLA({
            x: xd + x0,
            y: yd + y0,
            z: zd + z0
        })
    }
    _.prototype.radians = function (degree) {
        return this.PI / 180 * degree
    }
    return _
})()
export default ECEF
applications/task-work-order/src/utils/cesium/kmz.js
@@ -209,6 +209,11 @@
}
/**
 * 递归处理对象,移除 #text 键并返回其值,同时处理数值字符串到数字的转换
 * @param {any} obj - 需要处理的对象或值
 * @returns {any} 处理后的对象或值
 */
export function removeTextKey(obj) {
    if (typeof obj !== 'object' || obj === null) {
        // 如果是数值字符串,转换为数值
@@ -219,18 +224,21 @@
    }
    if (Array.isArray(obj)) {
        // 递归处理数组中的每个元素
        return obj.map(item => removeTextKey(item));
    }
    if (Object.prototype.hasOwnProperty.call(obj, '#text')) {
        // 如果 #text 的值是数值字符串,转换为数值
        // 如果存在 #text 属性,返回其值(如果是数值字符串则转换为数字)
        const textValue = obj['#text'];
        return typeof textValue === 'string' && /^-?\d+(\.\d+)?$/.test(textValue) ? Number(textValue) : textValue;
    }
    // 递归处理对象属性
    const newObj = {};
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            // 如果属性值是空对象,则设为空字符串;否则递归处理
            newObj[key] = Object.keys(obj[key]).length === 0 ? '' : removeTextKey(obj[key]);
        }
    }
applications/task-work-order/src/utils/cesium/mapUtil.js
@@ -1,7 +1,8 @@
import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
import store from '@/store'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
export const getLngLatDistance = (lat1, lng1, lat2, lng2) => {
    const radLat1 = (lat1 * Math.PI) / 180.0
    const radLat2 = (lat2 * Math.PI) / 180.0
@@ -190,6 +191,66 @@
    const newArr = arr.sort((a, b) => a.distance - b.distance)
    return newArr[0]
}
// 获取多边形内部数据
export const samplePolygonInterior = (positions, samplingDistance) => {
    if (!positions.length) return []
    let minLng = Infinity,
        maxLng = -Infinity
    let minLat = Infinity,
        maxLat = -Infinity
    positions.forEach(pos => {
        minLng = Math.min(minLng, pos.lng)
        maxLng = Math.max(maxLng, pos.lng)
        minLat = Math.min(minLat, pos.lat)
        maxLat = Math.max(maxLat, pos.lat)
    })
    //  将 Cesium 多边形格式转为 Turf 格式
    const turfPolygonCoords = positions.map(pos => [pos.lng, pos.lat])
    turfPolygonCoords.push([positions[0].lng, positions[0].lat]) // 闭合多边形
    const turfPolygon = turf.polygon([turfPolygonCoords])
    //  生成网格点并判断是否在多边形内
    const sampledPoints = []
    for (let lng = minLng; lng <= maxLng; lng += samplingDistance) {
        for (let lat = minLat; lat <= maxLat; lat += samplingDistance) {
            // 创建 Turf 点
            const turfPoint = turf.point([lng, lat])
            // Turf 原生方法判断点是否在多边形内
            if (turf.booleanPointInPolygon(turfPoint, turfPolygon)) {
                sampledPoints.push({
                    lng: Number(lng.toFixed(6)),
                    lat: Number(lat.toFixed(6)),
                    height: 0,
                })
            }
        }
    }
    return sampledPoints
}
// 获取多边形面内的最高点(相对地形高度)
export const getPolygonHighestPoint = async (positions, viewer, samplingDistance = 0.0002) => {
    try {
        const interiorPoints = samplePolygonInterior(positions, samplingDistance, viewer)
        const allPoints = [...positions, ...interiorPoints]
        // 批量获取所有点的地形高度
        const pointsWithTerrain = await getPointPositionsHeight(allPoints, viewer)
        // console.log('所有点的地形高度', pointsWithTerrain)
        // 地形高度(ASL)最高的点
        const highestTerrainPoint = pointsWithTerrain.reduce(
            (max, curr) => (curr.ASL > max.ASL ? curr : max),
            pointsWithTerrain[0]
        )
        // console.log('地形最高点信息:', highestTerrainPoint)
        return highestTerrainPoint
    } catch (error) {
        console.error('获取地形最高点失败:', error)
        return null
    }
}
// 获取当前经纬度地形数据
export const getLnglatAltitude = (longitude, latitude, viewer) => {
@@ -285,31 +346,24 @@
    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) {
                // updatedPositions 是一个数组,包含更新后的 Cartographic 对象
                //  是一个数组,包含更新后的 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,
@@ -317,18 +371,14 @@
                        ASL: Number(item.height),
                        customHeight: Number(item.height),
                    }
                    if (droneHeight) pointData.TH = Number(item.height) + Number(droneHeight)
                    return pointData
                })
                resolve(newPosition)
                // 在这里,你可以使用 height 值进行后续操作
            })
            .catch(function (error) {
                // console.error('获取高程时发生错误:', error);
                reject(error)
                console.log('获取高程时发生错误:', error)
                resolve(data.map(item => ({ ...item, ASL: 0, customHeight: 0, longitude: item.lng, latitude: item.lat })))
            })
    })
}
@@ -410,11 +460,15 @@
    if (!Array.isArray(positionsData) || positionsData.length === 0) return
    // 如果是一个点,加两个点方便后续生成外包围盒
    if (positionsData.length === 1){
        const [lon,lat,height] = positionsData[0]
        positionsData = [[lon+0.001,lat+0.001,height],[lon-0.001,lat-0.001,height],[lon,lat,height]]
    if (positionsData.length === 1) {
        const [lon, lat, height = 0] = positionsData[0]
        positionsData = [
            [lon + 0.001, lat + 0.001, height],
            [lon - 0.001, lat - 0.001, height],
            [lon, lat, height],
        ]
    }
    const positions = positionsData.map(([lon, lat, height]) =>
    const positions = positionsData.map(([lon, lat, height = 0]) =>
        Cesium.Cartesian3.fromDegrees(Number(lon), Number(lat), Number(height || 0))
    )
@@ -439,7 +493,7 @@
            range: boundingSphere.radius * multiple,
        },
        complete: () => {
            // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点
            // 飞行完成后检查相机高度是否小于最高点
            const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height
            if (cameraHeight < maxHeight) {
                viewer.camera.flyTo({
@@ -529,7 +583,7 @@
// 辅助函数:创建旋转文字的画布
export function createRotatedTextCanvas(time) {
    const prefix = '预计'
    const prefix = '预计到达'
    const suffix = '分钟'
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
@@ -537,7 +591,9 @@
    context.font = 'bold 16px Source Han Sans CN'
    // 测量各部分文本宽度
    const prefixWidth = context.measureText(prefix).width
    const timeWidth = context.measureText(time).width
    let timeWidth = context.measureText(time).width
    const timeWidthMore = `${time >= 10 ? 6 : 0}`
    timeWidth = timeWidth + Number(timeWidthMore)
    const suffixWidth = context.measureText(suffix).width
    const totalWidth = prefixWidth + timeWidth + suffixWidth
    const textHeight = 20 // 估算高度
@@ -550,17 +606,116 @@
    context.textAlign = 'left' // 改为左对齐,方便分段绘制
    // 清除画布
    context.clearRect(0, 0, canvas.width, canvas.height)
    // 绘制“预计”(白色)
    // 绘制“预计到达”(白色)
    context.fillStyle = '#FFFFFF'
    context.fillText(prefix, 10, 15)
    // 绘制时间(蓝色)
    context.font = 'bold 30px Source Han Sans CN'
    context.font = 'bold 24px Source Han Sans CN'
    context.fillStyle = '#1EE7E7' // 亮蓝色
    context.fillText(time, 10 + prefixWidth, 12)
    // 设置宽度
    context.textBaseline = 'middle'
    context.fillText(time, 10 + prefixWidth, 14)
    // 绘制“分钟”(白色)
    context.font = 'bold 16px Source Han Sans CN'
    context.fillStyle = '#FFFFFF'
    context.fillText(suffix, 20 + prefixWidth + timeWidth, 15)
    context.fillText(suffix, 14 + prefixWidth + timeWidth, 15)
    return canvas
}
export const saveCurrentCameraPosition = viewer => {
    const position = viewer?.camera.position.clone() // 相机位置(Cartesian3)
    const heading = viewer?.camera.heading // 航向角(弧度)
    const pitch = viewer?.camera.pitch // 俯仰角(弧度)
    const roll = viewer?.camera.roll // 翻滚角(弧度)
    store.commit('setCameraPosition', {
        duration: 0,
        destination: position,
        orientation: { heading, pitch, roll },
    })
}
// 连接地面线
export function getDockPolyLine(data, viewer) {
    if (!data.longitude || !data.latitude) return {}
    return {
        positions: new Cesium.CallbackProperty(() => {
            const pointPosition = Cesium.Cartesian3.fromDegrees(+data.longitude, +data.latitude, +data.height)
            if (!pointPosition) return []
            // 获取点的位置
            const cartographic = Cesium.Cartographic.fromCartesian(pointPosition)
            // 获取地形高度
            const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0
            // 创建地面点位置
            const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight)
            return [pointPosition, groundPosition]
        }, false),
        width: 0.5,
        material: Cesium.Color.WHITE,
    }
}
// 飞向一个机巢中心
export const flyDockCenter = (viewer, info) => {
    const { longitude = 115, latitude = 28, height = 0 } = info
    const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, +height)
    const droneEntity = viewer?.entities.add({ position: position, point: { pixelSize: 0 } })
    viewer?.flyTo(droneEntity, {
        offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 22000),
        duration: 0.5,
        complete: () => {
            viewer?.entities.remove(droneEntity)
        },
    })
}
// 在固定的经度上获得高度
export function getHeightOnFixedLonLat(ray, pointParams, ellipsoid = Cesium.Ellipsoid.WGS84) {
    const { longitude: lon, latitude: lat } = pointParams
    // 1. 地表点(高度=0)
    const p0 = Cesium.Cartesian3.fromDegrees(lon, lat, 0, ellipsoid)
    // 2. 该点处的法线方向(up 向量,单位向量)
    const up = ellipsoid.geodeticSurfaceNormal(p0, new Cesium.Cartesian3())
    // 3. 射线起点和方向
    const r0 = ray.origin // 相机位置
    const rd = ray.direction // 射线方向(已归一化?不一定,但不影响)
    // 4. 解:求直线 L(t) = p0 + t * up 与射线 R(s) = r0 + s * rd 的最近点
    // 使用向量公式求两条直线的最近点参数 t
    const w0 = Cesium.Cartesian3.subtract(p0, r0, new Cesium.Cartesian3())
    const a = Cesium.Cartesian3.dot(up, up) // = 1(因为 up 是单位向量)
    const b = Cesium.Cartesian3.dot(up, rd)
    const c = Cesium.Cartesian3.dot(rd, rd) // = |rd|^2
    const d = Cesium.Cartesian3.dot(up, w0)
    const e = Cesium.Cartesian3.dot(rd, w0)
    const denom = a * c - b * b
    let t
    if (Math.abs(denom) < Cesium.Math.EPSILON10) {
        // 两直线平行,取 t = 0
        t = 0
    } else {
        t = (b * e - c * d) / denom
    }
    // 5. 得到高度线上对应的点
    const resultPoint = Cesium.Cartesian3.add(
        p0,
        Cesium.Cartesian3.multiplyByScalar(up, t, new Cesium.Cartesian3()),
        new Cesium.Cartesian3()
    )
    // 6. 转回 Cartographic 获取精确高度
    const carto = Cesium.Cartographic.fromCartesian(resultPoint, ellipsoid)
    return {
        cartesian: resultPoint,
        height: carto.height,
        longitude: Cesium.Math.toDegrees(carto.longitude),
        latitude: Cesium.Math.toDegrees(carto.latitude),
    }
}
applications/task-work-order/src/utils/cesium/publicCesium.js
@@ -70,6 +70,7 @@
 * @param {boolean} [options.flyToContour=false] - 是否飞行到轮廓,默认为 false
 * @param {number} [options.multiple=1.4] - 范围倍数
 * @param {number} [options.dockOptions={}] - 机巢显示
 * @param {array} [options.terrainLoadCallback] - 地形加载回调
 * @param {array} [options.boundaryChange] - 边界改变事件
 * @param {array} [options.boundaryColor] - 边界颜色
 * @param {array} [options.dockRangeType] - 机巢范围类型 1显示覆盖圆;2显示覆盖圈
@@ -92,15 +93,14 @@
            dom,
            flatMode = true,
            terrain = false,
            layerMode = 0,
            layerMode = 17,
            contour = true,
            flyToContour = false,
            multiple = 1.4,
            dockOptions = {},
            boundaryColor = '#7FFFD4',
            boundaryChange,
            terrainLoadCallback,
            boundaryColor = '#7FFFD4',
            dockRangeType = 1,
            useDockHeight = false
        } = options
@@ -120,6 +120,10 @@
            baseLayer: false,
            fullscreenButton: false,
        }
        // 尝试解决出现多个id相同的dom
        const domIdList = document.querySelectorAll(`#${dom}`)
        if (domIdList.length>1) domIdList[1].remove()
        this.viewer = new Viewer(dom, viewerOptions)
        this.viewerDom = dom
@@ -143,33 +147,47 @@
        this.viewer?.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 禁用双击
        if (terrain) {
            // 正则:第一位非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)
                }
            }
            try {
                Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, {
                    requestVertexNormals: true, // 启用地形法线增强立体感
                    requestWaterMask: true, // 启用水体遮罩效果
                }).then(terrainProvider => {
                    this.viewer.terrainProvider = terrainProvider
                })
                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)
            }
@@ -180,6 +198,12 @@
        this.switchFlatMode(flatMode)
        this.switchContour(contour)
        flyToContour && this.flyToContour(contour)
        if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) {
            let dpr = window.devicePixelRatio
            while (dpr >= 2.0) dpr /= 2.0 // 避免过高缩放导致模糊
            this.viewer.resolutionScale = dpr // 设置分辨率缩放比例
        }
    }
    getViewer () {
@@ -188,53 +212,69 @@
    // 销毁viewer
    viewerDestroy (removeDom = true) {
        if (!this.viewer) return
        // // 👇 关键:先停止所有动态行为
        // this.viewer.clock.shouldAnimate = false;
        // this.viewer.scene.requestRenderMode = false;
        // // 取消所有可能的 pending 渲染
        // if (this.viewer.cesiumWidget) {
        //     this.viewer.cesiumWidget.render = () => {}; // 空函数覆盖
        // }
        // // 清理实体和图层
        // this.viewer?.entities.removeAll();
        // this.viewer?.imageryLayers.removeAll();
        // // 清理数据源
        // try {
        //     const dataSources = this.viewer.dataSources;
        //     while (dataSources.length > 0) {
        //         dataSources.remove(dataSources.get(0));
        //     }
        // } catch (e) {
        //     console.warn('清理数据源时出错:', e);
        // }
        // // 清理场景中的图元
        // try {
        //     this.viewer?.scene.primitives.removeAll();
        // } catch (e) {
        //     console.warn('清理图元时出错:', e);
        // }
        // // 获取webgl上下文
        // let gl = this.viewer?.scene.context._originalGLContext
        // if (gl) {
        //     try {
        //         gl.canvas.width = 1
        //         gl.canvas.height = 1
        //         gl.getExtension("WEBGL_lose_context").loseContext()
        //     } catch (e) {
        //         console.warn('释放WebGL上下文时出错:', e)
        //     }
        //     gl = null
        // }
        this.viewer?.destroy() // 销毁Viewer实例
        this.viewer = null
        smallMpaCenterDistance = null
        if (this.viewer) {
            this.viewer?.entities.removeAll()
            this.viewer?.imageryLayers.removeAll()
            this.viewer?.dataSources.removeAll()
            // try {
            //     this.viewer?.scene?.primitives?.removeAll()
            //     this.viewer?.entities?.removeAll()
            // } catch (e) {
            //     console.warn('清理资源时出错:', e)
            //     console.log(this.viewer, 1111111111111)
            // }
            // 获取webgl上下文
            let gl = this.viewer?.scene.context._originalGLContext
            gl.canvas.width = 1
            gl.canvas.height = 1
            gl.getExtension("WEBGL_lose_context").loseContext()
            gl = null
            this.viewer?.destroy() // 销毁Viewer实例
            this.viewer = null
            var cesiumContainer = document.getElementById(this.viewerDom)
            if (cesiumContainer && removeDom) {
                cesiumContainer.remove() // 移除与地图相关的DOM元素
                this.viewerDom = null
            }
        var cesiumContainer = document.getElementById(this.viewerDom)
        if (cesiumContainer && removeDom) {
            cesiumContainer.remove() // 移除与地图相关的DOM元素
            this.viewerDom = null
        }
    }
    setShowDock (sns) {
        this.boundary?.setShowDock(sns)
    }
    setDockCoverColor (sns) {
        this.boundary?.setDockCoverColor(sns)
    }
    // 切换轮廓显示
    async switchContour (open) {
        if (open) {
            await this.boundary?.openContour()
        } else {
            this.boundary?.closeContour()
        }
    }
    // // 飞向轮廓居中
    // 飞向轮廓居中
    flyToContour () {
        this.boundary?.flyToBoundary()
    }
@@ -257,7 +297,7 @@
        this.globalBaseMapLayers?.forEach(item => {
            if (item.mapLayer) item.mapLayer.show = false
        })
        // store.state.common.mapSetting.mode = mode
        store.state.common.mapSetting.mode = mode
        // 标准地图(天地图矢量)加载
        if (mode === 0) {
            mapLayers.push(
@@ -324,9 +364,7 @@
            this.viewer?.imageryLayers.lowerToBottom(find.mapLayer)
        })
    }
    // 设置或重置滤镜
    setCurrentLayerFilter (data, mode) {
        if (mode === 17) {
            data.mapLayer.brightness = 0.6 // 亮度(默认值)
applications/task-work-order/src/utils/cesium/use-kmz-tsa.js
New file
@@ -0,0 +1,675 @@
import _ from 'lodash'
import JSZIP from 'jszip'
import { JSONToXML } from './kmz'
import { getDateFromTimestamp } from '@/utils/date'
export default function kmzFile () {
    // 云台信息
    const droneModel = {
        52: '0',
        53: '1',
        66: '0',
        67: '1',
        80: '0',
        81: '1',
    }
    // 创建图斑航线
    const create = async (waylineBasicInfo, lnglats, spotList) => {
        try {
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const fileName = '图斑航线规划_' + date.getTime()
            // 无人机机型、云台型号、航线高度
            const { droneEnumValue, payloadEnumValue, height } = waylineBasicInfo
            const defaultHeight = height || 100
            // 起始点位 (参考点位-具体以机场为准)
            const startPoint = lnglats[0]
            const takeOffPoint = [startPoint[1], startPoint[0], 0].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'goContinue' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 20 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: { '#text': 'relativeToStartPoint' },
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': 'usePointSetting' },
                    // 全局高度
                    globalHeight: { '#text': '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    // payloadParam: {
                    //   payloadPositionIndex: { '#text': 0 },
                    //   focusMode: { '#text': 'firstPoint' },
                    //   meteringMode: { '#text': 'average' },
                    //   returnMode: { '#text': 'singleReturnFirst' },
                    //   samplingRate: { '#text': '240000' },
                    //   scanningMode: { '#text': 'repetitive' },
                    //   imageFormat: { '#text': 'wide,zoom' },
                    // },
                },
            }
            // 航点创建
            const polygonNo = {
                no: '',
                index: 0,
            }
            lnglats.forEach((lnglat, index) => {
                const obj = spotList.find(spot => spot.dkfw.find(item => _.isEqual(item, lnglat)))
                let fileSuffix = ''
                if (obj) {
                    if (polygonNo.no === obj.dkbh) {
                        polygonNo.index++
                    } else {
                        polygonNo.no = obj.dkbh
                        polygonNo.index = 1
                    }
                    fileSuffix = `图斑航线拍摄(${obj.dkbh}-${polygonNo.index})`
                } else {
                    fileSuffix = '图斑航线拍摄(起点)'
                }
                const placemark = {
                    Point: {
                        // 航点经纬度
                        coordinates: { '#text': `${lnglat[0]},${lnglat[1]}` },
                    },
                    // 航点index
                    index: { '#text': index },
                    // 全局航线高度(椭球高)
                    ellipsoidHeight: { '#text': defaultHeight },
                    // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                    height: { '#text': defaultHeight },
                    // 航点飞行速度
                    waypointSpeed: { '#text': 10 },
                    // 沿航线方向 - 可使用当前默认值
                    waypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 航点类型
                    waypointTurnParam: {
                        waypointTurnMode: {
                            '#text': 'toPointAndStopWithDiscontinuityCurvature',
                        },
                        waypointTurnDampingDist: { '#text': 0.2 },
                    },
                    // 是否全局参数
                    useGlobalHeight: { '#text': 1 },
                    useGlobalSpeed: { '#text': 1 },
                    useGlobalHeadingParam: { '#text': 1 },
                    useGlobalTurnParam: { '#text': 1 },
                    useStraightLine: { '#text': 1 },
                    // 事件组
                    actionGroup: {
                        actionGroupId: { '#text': index },
                        actionGroupStartIndex: { '#text': index },
                        actionGroupEndIndex: { '#text': index },
                        actionGroupMode: { '#text': 'sequence' },
                        actionTrigger: {
                            actionTriggerType: { '#text': 'reachPoint' },
                        },
                        // 单个事件 - 如果是多个事件action可改成数组 action: [{}]
                        action: {
                            actionId: { '#text': 0 },
                            actionActuatorFunc: { '#text': 'takePhoto' },
                            actionActuatorFuncParam: {
                                fileSuffix: { '#text': fileSuffix },
                                payloadPositionIndex: { '#text': '0' },
                                useGlobalPayloadLensIndex: { '#text': '1' },
                            },
                        },
                    },
                }
                fileTemplate.Folder.Placemark.push(placemark)
            })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return false
        }
    }
    // 创建正常航线
    const createNoramlWaylines = (waylineBasicInfo, lnglats) => {
        try {
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime)
            const fileName = '新建航线_' + year + month + day + hours + minutes
            // 无人机机型、云台型号、航线高度
            const { droneEnumValue, payloadEnumValue, height, waylineName } = waylineBasicInfo
            const defaultHeight = height || 100
            // 起始点位 (参考点位-具体以机场为准)
            const startPoint = lnglats[0]
            const takeOffPoint = [startPoint.latitude, startPoint.longitude, startPoint.height].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'goContinue' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 20 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞)
                    autoRerouteInfo: {
                        missionAutoRerouteMode: { '#text': 1 },
                        transitionalAutoRerouteMode: { '#text': 1 },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: { '#text': 'relativeToStartPoint' },
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': 'usePointSetting' },
                    // 全局高度
                    globalHeight: { '#text': '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    payloadParam: {
                        payloadPositionIndex: { '#text': 0 },
                        focusMode: { '#text': 'firstPoint' },
                        meteringMode: { '#text': 'average' },
                        returnMode: { '#text': 'singleReturnFirst' },
                        samplingRate: { '#text': '240000' },
                        scanningMode: { '#text': 'repetitive' },
                        imageFormat: { '#text': 'wide,zoom' },
                    },
                },
            }
            lnglats.forEach((lnglat, index) => {
                const placemark = {
                    Point: {
                        // 航点经纬度
                        coordinates: {
                            '#text': `${lnglat.longitude},${lnglat.latitude}`,
                        },
                    },
                    // 航点index
                    index: { '#text': index },
                    // 全局航线高度(椭球高)
                    ellipsoidHeight: { '#text': lnglat.height },
                    // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                    height: { '#text': lnglat.height },
                    // 航点飞行速度
                    waypointSpeed: { '#text': lnglat?.speed || 10 },
                    // 沿航线方向 - 可使用当前默认值
                    waypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 航点类型
                    waypointTurnParam: {
                        waypointTurnMode: {
                            '#text': 'toPointAndStopWithDiscontinuityCurvature',
                        },
                        waypointTurnDampingDist: { '#text': 0.2 },
                    },
                    // 是否全局参数
                    useGlobalHeight: { '#text': 1 },
                    useGlobalSpeed: { '#text': 0 },
                    useGlobalHeadingParam: { '#text': 1 },
                    useGlobalTurnParam: { '#text': 1 },
                    useStraightLine: { '#text': 1 },
                }
                if (lnglat?.actions) {
                    const actionGroup = {
                        actionGroupId: { '#text': index },
                        actionGroupStartIndex: { '#text': index },
                        actionGroupEndIndex: { '#text': index },
                        actionGroupMode: { '#text': 'sequence' },
                        actionTrigger: {
                            actionTriggerType: { '#text': 'reachPoint' },
                        },
                        // 单个事件 - 如果是多个事件action可改成数组 action: [{}]
                        // action: [{
                        //   actionId: { '#text': 0 },
                        //   actionActuatorFunc: { '#text': '' },
                        //   actionActuatorFuncParam: {},
                        // }],
                        action: [],
                    }
                    lnglat?.actions.forEach((action, index) => {
                        const singleAction = {
                            actionId: { '#text': index },
                            actionActuatorFunc: { '#text': '' },
                            actionActuatorFuncParam: {},
                        }
                        // actionGroup.action['actionActuatorFunc'] = { '#text': action.key }
                        // action.params.forEach(p => {
                        //   if (p.value || p.value !== '') {
                        //     actionGroup.action['actionActuatorFuncParam'][p.key] = { '#text': p.value }
                        //   }
                        // })
                        singleAction['actionActuatorFunc'] = {
                            '#text': action.key,
                        }
                        action.params.forEach(p => {
                            if (p.value || p.value !== '') {
                                singleAction['actionActuatorFuncParam'][p.key] = {
                                    '#text': p.value,
                                }
                            }
                        })
                        if (lnglat?.actions.length > 1) {
                            actionGroup.action.push(singleAction)
                        } else {
                            actionGroup.action = singleAction
                        }
                    })
                    placemark.actionGroup = actionGroup
                }
                fileTemplate.Folder.Placemark.push(placemark)
            })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName: waylineName || fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return error
        }
    }
    // 创建面状航线
    const createPlanarWayline = (waylineBasicInfo, lnglats) => { }
    // 创建点状航线
    const createPointWayLines = (waylineBasicInfo, points, settingInfo, dockPosition) => {
        try {
            const { droneEnumValue, payloadEnumValue, waylineName } = waylineBasicInfo
            const {
                imageFormat,
                heightMode,
                globalHeight,
                autoFlightSpeed,
                gimbalPitchMode,
                waypointTurnMode,
                waypointHeadingMode,
                finishAction,
                globalTransitionalSpeed,
            } = settingInfo
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime)
            const fileName = '新建航线_' + year + month + day + hours + minutes
            // 无人机机型、云台型号、航线高度
            const defaultHeight = 100
            // 起始点位 (参考点位-具体以机场为准)
            const takeOffPoint = [dockPosition.latitude, dockPosition.longitude, 0].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': finishAction || 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'executeLostAction' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 120 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': globalTransitionalSpeed || 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞)
                    autoRerouteInfo: {
                        missionAutoRerouteMode: { '#text': 1 },
                        transitionalAutoRerouteMode: { '#text': 1 },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: heightMode,
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': autoFlightSpeed || 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': gimbalPitchMode || 'manual' },
                    // 全局高度
                    globalHeight: { '#text': globalHeight || '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': waypointHeadingMode || 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    payloadParam: {
                        payloadPositionIndex: { '#text': 0 },
                        focusMode: { '#text': 'firstPoint' },
                        meteringMode: { '#text': 'average' },
                        returnMode: { '#text': 'singleReturnFirst' },
                        samplingRate: { '#text': '240000' },
                        scanningMode: { '#text': 'repetitive' },
                        imageFormat: imageFormat,
                    },
                },
            }
            points.length &&
                points.forEach((detail, ind) => {
                    const { Point, index, height, ellipsoidHeight, actionGroup, waypointSpeed } = detail
                    const placemark = {
                        Point,
                        // 航点index
                        index,
                        // 全局航线高度(椭球高)
                        ellipsoidHeight,
                        // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                        height,
                        // 航点飞行速度
                        waypointSpeed: { '#text': waypointSpeed || 10 },
                        // 沿航线方向 - 可使用当前默认值
                        waypointHeadingParam: {
                            waypointHeadingMode: {
                                '#text': waypointHeadingMode || 'followWayline',
                            },
                            waypointHeadingAngle: { '#text': 0 },
                            waypointPoiPoint: {
                                '#text': '0.000000,0.000000,0.000000',
                            },
                            waypointHeadingPathMode: { '#text': 'followBadArc' },
                            waypointHeadingPoiIndex: { '#text': 0 },
                        },
                        // 航点类型
                        waypointTurnParam: {
                            waypointTurnMode: {
                                '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature',
                            },
                            waypointTurnDampingDist: { '#text': 0.2 },
                        },
                        // 是否全局参数
                        useGlobalHeight: { '#text': 1 },
                        useGlobalSpeed: { '#text': 0 },
                        useGlobalHeadingParam: { '#text': 1 },
                        useGlobalTurnParam: { '#text': 1 },
                        useStraightLine: { '#text': 1 },
                    }
                    if (actionGroup) {
                        placemark.actionGroup = actionGroup
                    }
                    fileTemplate.Folder.Placemark.push(placemark)
                })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName: waylineName || fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return error
        }
    }
    const save = async fileInfo => {
        const JsZip = new JSZIP()
        const { fileName, template, waylines } = fileInfo
        const templateXml = JSONToXML(template, '', true)
        const waylinesWpml = JSONToXML(waylines, '', true)
        JsZip.file('wpmz/template.kml', templateXml)
        JsZip.file('wpmz/waylines.wpml', waylinesWpml)
        const fileBlob = await JsZip.generateAsync({ type: 'blob' })
        // saveAs(kmzFile, `${fileName}.kmz`)
        return fileBlob
    }
    const waylinesContext = templateJson => {
        const waylinesObj = {
            // 标准模版
            missionConfig: {
                flyToWaylineMode: null,
                finishAction: null,
                exitOnRCLost: null,
                executeRCLostAction: null,
                takeOffSecurityHeight: null,
                globalTransitionalSpeed: null,
                globalRTHHeight: null,
                droneInfo: null,
                payloadInfo: null,
            },
            Folder: {
                templateId: null,
                executeHeightMode: null,
                waylineId: null,
                autoFlightSpeed: null,
                /*
          waylines.wpml和template.xml的Placemark通用
          不同点:
            waylineId = templateId
            ellipsoidHeight、 height 替换成 executeHeight
        */
                Placemark: null,
            },
        }
        Object.keys(waylinesObj).forEach(key => {
            if (Object.prototype.toString.call(waylinesObj[key]) === '[object Object]') {
                Object.keys(waylinesObj[key]).forEach(item => {
                    waylinesObj[key][item] = templateJson[key][item]
                    if (item === 'executeHeightMode') {
                        waylinesObj[key][item] = templateJson[key].waylineCoordinateSysParam.heightMode
                    }
                    if (item === 'waylineId') {
                        waylinesObj[key][item] = templateJson[key].templateId
                    }
                    if (item === 'Placemark') {
                        const placemarks = _.cloneDeep(templateJson[key][item])
                        placemarks.forEach(placemark => {
                            placemark.executeHeight = {
                                '#text': placemark.ellipsoidHeight?.['#text'] || '',
                            }
                            delete placemark.ellipsoidHeight
                            delete placemark.height
                        })
                        waylinesObj[key][item] = placemarks
                    }
                })
            } else {
                waylinesObj[key] = templateJson[key]
            }
        })
        return waylinesObj
    }
    return {
        create,
        createNoramlWaylines,
        createPlanarWayline,
        createPointWayLines,
        save,
    }
}
applications/task-work-order/src/utils/cesium/useBoundary.js
@@ -6,19 +6,17 @@
import { useStore } from 'vuex'
import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation'
import userStore from '@/store/modules/user'
import { areaCodeToArr, getContourByCode } from '@/utils/cesium/mapUtil'
// import { getEllipse } from '@/hooks/useSingleDroneMap/useSingleDroneMap'
import { areaCodeToArr, getContourByCode, getDockPolyLine, getLnglatAltitude } from '@/utils/cesium/mapUtil'
import getBaseConfig from '@/buildConfig/config'
import { getDroneStatusImage } from '@/utils/stateToImageMap/drone'
import {
    GroundCirclePrimitiveManager
} from '@/utils/mapUtils'
import { GroundCirclePrimitiveManager } from '@/utils/mapUtils'
import lowBattery from '@/assets/images/aiNowFly/low-battery.svg'
import mediumBattery from '@/assets/images/aiNowFly/medium-battery.svg'
import fullBattery from '@/assets/images/aiNowFly/full-charge.svg'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
// const { singleDockSystem } = getBaseConfig()
const { singleDockSystem } = getBaseConfig()
/**
 * 使用通用的边界
@@ -26,8 +24,15 @@
 * @param options
 */
export const useBoundary = (viewer, options = {}) => {
    const { multiple = 1.4, dockOptions = {}, boundaryChange, boundaryColor, dockRangeType, useDockHeight = false } = options
    const { scrollShowDock = false, showDock = false } = dockOptions
    const {
        multiple = 1.4,
        dockOptions = {},
        boundaryChange,
        boundaryColor,
        dockRangeType,
        useDockHeight = false,
    } = options
    const { scrollShowDock = false, showDock = false, showDockNameAndBattery = false } = dockOptions
    const manager = new GroundCirclePrimitiveManager()
    manager.init(viewer)
@@ -56,7 +61,7 @@
    }
    // 获取设备列表
    async function getDeviceRegionFun () {
    async function getDeviceRegionFun() {
        const res = await getDeviceRegion({ areaCode: selectedAreaCode })
        return res?.data?.data || []
    }
@@ -100,60 +105,127 @@
        return await res.json()
    }
    function getBase64Image(imgUrl) {
        const img = new Image()
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        return new Promise(resolve => {
            img.onload = function () {
                canvas.width = img.width
                canvas.height = img.height
                ctx.drawImage(img, 0, 0)
                const base64 = canvas.toDataURL('image/png')
                resolve(base64)
            }
            img.src = imgUrl
        })
    }
    function batteryLevelSvg(batteryNum) {
        if (batteryNum <= 30) {
            return lowBattery
        } else if (batteryNum > 30 && batteryNum <= 60) {
            return mediumBattery
        } else {
            return fullBattery
        }
    }
    async function nestSVG(nickname, batteryImgUrl, batteryNum) {
        let txt = nickname.length < 4 ? 130 : nickname.length * 38
        let tranx = nickname.length < 4 ? 86 : nickname.length * 26 + 3
        const batteryBase64Image = await getBase64Image(batteryImgUrl)
        const svg = `
          <svg width="${txt}" height="100" xmlns="http://www.w3.org/2000/svg">
            <g transform="translate(${tranx}, 10)">
                <!-- 电池图标 -->
                <image href="${batteryBase64Image}" x="0" y="2" width="12" height="13"/>
                <!-- 电量值 -->
                <text x="12" y="10" font-size="14" fill="${
                    batteryNum <= 30 ? '#FF604B' : batteryNum >= 60 ? '#40FF5C' : '#00FFF2'
                }"  text-anchor="start" dominant-baseline="middle" font-weight="bolder" font-family="Source Han Sans CN" text-rendering="geometricPrecision">
                    ${batteryNum}%
                </text>
            </g>
          </svg>
        `
        return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
    }
    // 无人机散点渲染
    const droneSplashed = () => {
        let showDockList = showDockSnList === null
            ? _.cloneDeep(initDockList)
            : initDockList.filter(item => showDockSnList.includes(item.device_sn))
        manager.removeAll()
        let showDockList =
            showDockSnList === null
                ? _.cloneDeep(initDockList)
                : initDockList.filter(item => showDockSnList.includes(item.device_sn))
        showDockList.forEach((item, index) => {
        showDockList.forEach(async (item, index) => {
            if (item.status === 'OFFLINE') return
            let polyline = {}
            if (dockRangeType === 2) {
                manager.addCircleOutline({
                    data: {
                        lng: item.longitude,
                        lat: item.latitude
                        lat: item.latitude,
                    },
                    frameColor: Cesium.Color.fromCssColorString('#7FFFD4')
                    frameColor: Cesium.Color.fromCssColorString('#7FFFD4'),
                })
            } else {
                manager.addCircle({
                    data: {
                        lng: item.longitude,
                        lat: item.latitude
                        lat: item.latitude,
                    },
                    materialColor: Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3)
                    materialColor: item.color ? item.color : Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3),
                })
            }
            if (useDockHeight) {
                polyline = {
                    positions: new Cesium.CallbackProperty(() => {
                        const pointPosition = Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, +item.height)
                        if (!pointPosition) return []
                        // 获取点的位置
                        const cartographic = Cesium.Cartographic.fromCartesian(pointPosition)
                        // 获取地形高度
                        const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0
                        // 创建地面点位置
                        const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight)
                        return [pointPosition, groundPosition]
                    }, false),
                    width: 0.5,
                    material: Cesium.Color.WHITE,
                }
                polyline = getDockPolyLine(item, viewer)
            }
            const batteryImgUrl = batteryLevelSvg(item.capacity_percent)
            const combinedImage = await nestSVG(item.nickname, batteryImgUrl, item.capacity_percent)
            let pointObj = {}
            try {
                pointObj = await getLnglatAltitude(item.longitude, item.latitude, viewer)
            } catch (e) {}
            const position = Cesium.Cartesian3.fromDegrees(
                +item.longitude,
                +item.latitude,
                useDockHeight ? +item.height : pointObj?.height || 0
            )
            if (showDockNameAndBattery) {
                // 带电量
                dockSource.entities.add({
                    position,
                    billboard: {
                        // new Cesium.ConstantProperty(pointData.status === 'OFFLINE' ? endingImg : unSelectedOnline),
                        image: combinedImage,
                    },
                })
            }
            dockSource.entities.add({
                position: Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, !useDockHeight ? 0 : +item.height),
                label: {
                    text: item.nickname,
                    font: 'bold 16px Source Han Sans CN',
                    fillColor: Cesium.Color.WHITE,
                    outlineColor: Cesium.Color.BLACK,
                    outlineWidth: 2,
                    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                    pixelOffset: new Cesium.Cartesian2(0, -24),
                },
                position,
                polyline,
                billboard: {
                    image: getDroneStatusImage(item.status),
                    width: 60,
                    height: 60,
                    eyeOffset: new Cesium.Cartesian3(0, 0, -5)
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    eyeOffset: new Cesium.Cartesian3(0, 0, -5),
                },
            })
        })
@@ -186,7 +258,7 @@
                            positions: positions,
                            width: 5, // 直接设置宽度
                            clampToGround: true,
                            material: Cesium.Color.fromCssColorString(boundaryColor)
                            material: Cesium.Color.fromCssColorString(boundaryColor),
                        },
                        polygon,
                    })
@@ -205,7 +277,7 @@
                        positions: positions,
                        width: 1, // 直接设置宽度
                        clampToGround: true,
                        material: Cesium.Color.fromCssColorString(boundaryColor)
                        material: Cesium.Color.fromCssColorString(boundaryColor),
                    },
                })
            })
@@ -217,7 +289,6 @@
    // 打开边界
    const openContour = async () => {
        const areaCode = selectedAreaCode || userAreaCode
        viewer.scene.postRender.removeEventListener(determineScaling)
        if (!areaCode) return
        const hierarchy = areaCodeToArr(areaCode.slice(0, 6))
@@ -258,25 +329,29 @@
            const outlineGJson = await getContourByCode(areaCode.slice(0, 6))
            scalingJudgment.forEach(item => item.show && (item.outline = outlineGJson))
            renderOutline(scalingJudgment[(hierarchy.length - 3) * -1])
        } catch (e) { }
        viewer.scene.postRender.addEventListener(determineScaling)
        } catch (e) {}
        try {
            viewer?.scene?.postRender?.addEventListener(determineScaling)
        }catch (e) {
        }
    }
    // 关闭边界
    function closeContour () {
    function closeContour() {
        removeAllEntities()
        viewer.scene.postRender.removeEventListener(determineScaling)
    }
    // 飞向边界
    async function flyToBoundary () {
    async function flyToBoundary() {
        // 单机巢系统
        if (singleDockSystem) {
            if (!initDockList.length) {
                initDockList = await getDeviceRegionFun()
            }
            viewer?.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 24000),
                destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 22000),
                duration: 0,
            })
            return
@@ -286,7 +361,8 @@
        const dataSource = await Cesium.GeoJsonDataSource.load(gJson)
        if (!dataSource) return
        viewer.dataSources.add(dataSource)
        // 获取多边形边界所有点
        let positionList = []
        dataSource.entities.values.forEach(function (entity) {
            if (entity.polygon) {
                // 获取多边形的边界球
@@ -295,18 +371,16 @@
                    let cartographic = Cesium.Cartographic.fromCartesian(item)
                    let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度
                    let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度
                    return [_.round(lng, 6), _.round(lat, 6)]
                })
                const newBox = boxTransformScale(curPolygonPosition, multiple)
                viewer.camera.flyTo({
                    destination: Cesium.Rectangle.fromDegrees(...newBox),
                    offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
                    duration: 0.5,
                })
                positionList = positionList.concat(curPolygonPosition)
            }
        })
        const newBox = boxTransformScale(positionList, multiple)
        viewer.camera.flyTo({
            destination: Cesium.Rectangle.fromDegrees(...newBox),
            offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
            duration: 0.5,
        })
        dataSource.entities.values.forEach(entity => {
@@ -320,7 +394,7 @@
     * 设置要显示的机巢列表
     * @param dockSns - 要显示的机巢设备序列号数组。如果为 undefined,则显示所有已初始化的机巢;如果为空数组,则不显示任何机巢。
     */
    function setShowDock (dockSns) {
    function setShowDock(dockSns) {
        if (dockSns === undefined) {
            showDockSnList = null
        } else if (dockSns.length === 0) {
@@ -328,21 +402,31 @@
        } else {
            showDockSnList = dockSns
        }
        removeDockCover()
        if (active === '县') droneSplashed()
        if (!showDock) return
        if (scrollShowDock ? active === '县' : true) {
            droneSplashed()
        }
    }
    function setDockCoverColor(arr) {
        initDockList.forEach(item => {
            arr.forEach(dock => {
                item.color = dock.device_sn === item.device_sn ? dock.color : null
            })
        })
        if (scrollShowDock ? active === '县' : true) {
            droneSplashed()
        }
    }
    onBeforeUnmount(() => {
        manager.destroy()
    })
    return {
        openContour,
        closeContour,
        flyToBoundary,
        setShowDock
        setShowDock,
        setDockCoverColor,
    }
}
applications/task-work-order/src/views/RoutePlan/PlanarAirLine/planarRouteUtils.js
File was deleted
applications/task-work-order/src/views/RoutePlan/PointAirLine/pointWayLineUtils.js
File was deleted
applications/task-work-order/src/views/algorithmMange/algorithmMange.vue
File was deleted
applications/task-work-order/src/views/algorithmRepository/algorithmRepository.vue
File was deleted
applications/task-work-order/src/views/authority/apiscope.vue
File was deleted
applications/task-work-order/src/views/authority/datascope.vue
File was deleted
applications/task-work-order/src/views/authority/role.vue
File was deleted
applications/task-work-order/src/views/base/region.vue
File was deleted
applications/task-work-order/src/views/dataCenter/components/dataCenterMap.vue
File was deleted
applications/task-work-order/src/views/dataCenter/components/searchData.vue
File was deleted
applications/task-work-order/src/views/dataCenter/dataCenter.vue
File was deleted
applications/task-work-order/src/views/dataCockpit/components/CenterContainer.vue
New file
@@ -0,0 +1,237 @@
<template>
    <div class="center-container">
        <div class="left-box">
            <div class="logo">
                <img :src="warningLogo" alt="">
            </div>
            <div class="list">
                <div class="item" v-for="(item, ind) in warningList" :key="ind">
                    <div class="val" @click="warningTypeClick(item.type)">
                        {{ item.value }}
                    </div>
                    <div class="name">
                        {{ item.title }}
                    </div>
                </div>
            </div>
        </div>
        <div class="right-box">
            <div class="logo">
                <img :src="equipmentLogo" alt="">
            </div>
            <div class="list">
                <div class="item" v-for="(item, ind) in equipmentList" :key="ind">
                    <div class="val" :class="{highlight: item.isHighlight}" @click="equipmentClick(item.isHighlight)">
                        {{ item.value }}
                    </div>
                    <div class="name">
                        {{ item.title }}
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import warningLogo from '@/assets/images/dataCockpit/warning.png'
import equipmentLogo from '@/assets/images/dataCockpit/equipment.png'
import { alarmStatisticsApi } from '@/api/dataCockpit'
const warningList = ref([
    {
        title: '实时告警',
        value: 0,
        type: 1
    },
    {
        title: '设备告警',
        value: 0,
        type: 2
    },
    {
        title: '历史告警',
        value: 0,
        type: 3
    },
])
const equipmentList = ref([
    {
        title: '设备统计',
        value: 0,
        isHighlight: true,
    },
    {
        title: '无线电',
        value: 0,
    },
    {
        title: '光电',
        value: 0,
    },
    {
        title: '雷达',
        value: 0,
    }
])
const emit = defineEmits(['select-warning', 'select-equipment'])
const warningTypeClick = (type) => {
    emit('select-warning', type)
}
const equipmentClick = (isHighlight) => {
    if (isHighlight) {
        emit('select-equipment')
    }
}
const updateStats = async () => {
    const res = await alarmStatisticsApi()
    const data = res?.data?.data || {}
    warningList.value = [
        { title: '实时告警', value: data.realTimeAlarmCount ?? 0, type: 1 },
        { title: '设备告警', value: data.deviceAlarmCount ?? 0, type: 2 },
        { title: '历史告警', value: data.historyAlarmCount ?? 0, type: 3 }
    ]
    equipmentList.value = [
        { title: '设备统计', value: data.deviceCount ?? 0, isHighlight: true },
        { title: '无线电', value: data.radioCount ?? 0 },
        { title: '光电', value: data.opticalCount ?? 0 },
        { title: '雷达', value: data.radarCount ?? 0 }
    ]
}
onMounted(() => {
    updateStats()
})
</script>
<style lang="scss" scoped>
.center-container {
    display: flex;
    position: absolute;
    top: 75px;
    left: 465px;
    z-index: 9;
    pointer-events: none;
    .left-box {
        width: 356px;
        .list {
            .item {
                .val {
                    color: #FF4444;
                    cursor: pointer;
                }
            }
        }
    }
    .right-box {
        margin-left: 20px;
        width: 455px;
        .list {
            .item {
                .val.highlight {
                    color: #6382FF;
                    cursor: pointer;
                }
            }
        }
    }
    .left-box,
    .right-box {
        position: relative;
        padding: 17px 20px;
        display: flex;
        align-items: center;
        height: 100px;
        background: rgba(0,0,0,0.3);
        box-sizing: border-box;
        border-radius: 16px 16px 16px 16px;
        pointer-events: auto;
        &::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: -1;
            background: rgba(17, 23, 34, 0.56);
              backdrop-filter: blur(2px);
            border-radius: 16px 16px 16px 16px;
        }
        .logo {
            img {
                width: 40px;
                height: 40px;
            }
        }
        .list {
            width: 0;
            flex: 1;
            height: 100%;
            display: flex;
            .item {
                width: 0;
                flex: 1;
                position: relative;
                padding-left: 20px;
                color: #fff;
                .name {
                    font-family: Open Sans, Open Sans;
                    font-weight: 400;
                    font-size: 14px;
                    color: #FFFFFF;
                    text-align: left;
                    font-style: normal;
                    text-transform: none;
                }
                .val {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: bold;
                    font-size: 30px;
                    text-align: left;
                    font-style: normal;
                    text-transform: none;
                }
                &::after {
                    content: '';
                    position: absolute;
                    top: 0;
                    right: 0;
                    width: 1px;
                    height: 100%;
                    width: 1px;
                    background: #333355;
                }
                &:last-child::after {
                    display: none;
                }
            }
        }
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/DeviceHistoryDialog.vue
New file
@@ -0,0 +1,208 @@
<template>
    <el-dialog class="ztzf-data-cockpit-dialog" append-to-body v-model="visibleModel" :title="dialogTitle"
        :close-on-click-modal="false" :destroy-on-close="true">
        <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-dialog-history-search">
            <el-form-item label="关键字" prop="keyword">
                <el-input class="ztzf-data-cockpit-search-input" v-model="searchParams.keyword" placeholder="名称/序列号"
                    clearable @clear="handleSearch" />
            </el-form-item>
            <el-form-item prop="dateRange">
                <el-date-picker class="ztzf-data-cockpit-date-picker"
                    popper-class="ztzf-data-cockpit-tree-select-popper" v-model="searchParams.dateRange"
                    type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
                    value-format="YYYY/MM/DD" @change="handleSearch" />
            </el-form-item>
            <el-form-item label="区域" prop="area">
                <el-tree-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-tree-select-popper"
                    v-model="searchParams.area" :data="areaTree" :props="areaTreeProps" node-key="value" check-strictly
                    clearable placeholder="请选择" @change="handleSearch" />
            </el-form-item>
            <el-form-item label="无人机类型" prop="droneType">
                <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.droneType" placeholder="请选择" clearable @change="handleSearch">
                    <el-option v-for="item in droneTypeOptions" :key="item.value" :label="item.label"
                        :value="item.value" />
                </el-select>
            </el-form-item>
            <el-form-item class="history-search-actions">
                <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                <el-button :icon="Search" @click="handleSearch"></el-button>
            </el-form-item>
        </el-form>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
            <div class="ztzf-table-content">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" width="60" label="序号" />
                    <el-table-column prop="droneName" label="无人机名称" min-width="120" />
                    <el-table-column prop="droneSerialNo" label="无人机序列号" min-width="140" />
                    <el-table-column label="无人机类型" min-width="110">
                        <template #default="{ row }">
                            {{ getDroneTypeText(row.droneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="areaName" label="区域" min-width="90" />
                    <el-table-column prop="deviceName" label="数据来源" min-width="110" />
                    <el-table-column label="信号频段" min-width="100">
                        <template #default="{ row }">
                            {{ formatFrequency(row.signalFreqMhz) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="alarmTime" label="发现时间" min-width="150" />
                    <el-table-column prop="stayDuration" label="停留时间" min-width="110" />
                    <el-table-column label="反制方式" min-width="110">
                        <template #default="{ row }">
                            {{ getCounterWayText(row.counterWay) }}
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <div class="ztzf-table-pagination">
                <el-pagination popper-class="ztzf-data-cockpit-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>
    </el-dialog>
</template>
<script setup>
import { Search, RefreshRight } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
import { alarmLogApi } from '@/api/dataCockpit'
const props = defineProps({
    modelValue: {
        type: Boolean,
        default: false
    },
    device: {
        type: Object,
        default: () => null
    }
})
const emit = defineEmits(['update:modelValue'])
const visibleModel = computed({
    get: () => props.modelValue,
    set: (val) => emit('update:modelValue', val)
})
const dialogTitle = computed(() => {
    return `反制设备名称 ${props.device?.name || ''}`.trim()
})
const initSearchParams = () => ({
    keyword: '',
    dateRange: [],
    area: '',
    droneType: '',
    current: 1,
    size: 10
})
const searchParams = ref(initSearchParams())
const queryParamsRef = ref(null)
const loading = ref(true)
const list = ref([])
const total = ref(0)
const areaTree = ref([
    {
        label: '古村区',
        value: '古村区'
    },
    {
        label: '新区',
        value: '新区'
    },
    {
        label: '产业园',
        value: '产业园'
    }
])
const areaTreeProps = {
    label: 'label',
    children: 'children'
}
const droneTypeOptions = [
    { label: '微型机', value: '1' },
    { label: '植保机', value: '2' }
]
const getList = async () => {
    if (!props.device?.id) {
        list.value = []
        total.value = 0
        return
    }
    const [startTime, endTime] = searchParams.value.dateRange || []
    const keyword = (searchParams.value.keyword || '').trim()
    const params = {
        alarmType: '2',
        deviceId: props.device.id,
        current: searchParams.value.current,
        size: searchParams.value.size,
        startTime,
        endTime,
        areaName: searchParams.value.area || undefined,
        droneType: searchParams.value.droneType || undefined,
        droneName: keyword || undefined,
    }
    loading.value = true
    try {
        const res = await alarmLogApi(params)
        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 () {
    searchParams.value = initSearchParams()
    getList()
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHZ`
}
const getCounterWayText = (value) => {
    if (value === 1 || value === '1') return '信号干扰'
    if (value === 2 || value === '2') return '诱导驱离'
    if (value === 3 || value === '3') return '无'
    return value || '-'
}
const getDroneTypeText = (value) => {
    if (value === 1 || value === '1') return '微型机'
    if (value === 2 || value === '2') return '植保机'
    return value || '-'
}
watch(
    () => visibleModel.value,
    (visible) => {
        if (visible) {
            searchParams.value.current = 1
            getList()
        }
    }
)
</script>
<style lang="scss" scoped></style>
applications/task-work-order/src/views/dataCockpit/components/EmptyState.vue
New file
@@ -0,0 +1,59 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-09 15:06:02
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 15:10:44
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\EmptyState.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-09 15:06:02
-->
<template>
    <div class="empty-state">
        <div class="empty-content">
            <img :src="noDataBg" alt="暂无数据">
            <span class="empty-text">暂无数据</span>
        </div>
    </div>
</template>
<script setup>
import noDataBg from '@/assets/images/no-data-bg.png'
</script>
<style scoped lang="scss">
.empty-state {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    width: 100%;
    height: 100%;
    min-height: 220px;
    .empty-content {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        text-align: center;
    }
    img {
        max-width: 78%;
        max-height: 78%;
        object-fit: contain;
    }
    .empty-text {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 14px;
        color: #9E9EBA;
        line-height: 18px;
        text-align: center;
        font-style: normal;
        text-transform: none;
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/EquipmentWarning.vue
New file
@@ -0,0 +1,101 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:29:07
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 14:49:25
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\EquipmentWarning.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-08 09:29:07
-->
<template>
    <div
        class="equipment-warning"
        v-loading="loading"
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <EmptyState v-if="!equipmentWarningData.length" />
        <EquipmentTemplate
            v-else
            v-for="(item, ind) in equipmentWarningData"
            :key="ind"
            :data="item"
            @click="onCardClick"
        />
    </div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import EquipmentTemplate from './templateComponents/EquipmentTemplate.vue'
import { deviceSearchApi } from '@/api/dataCockpit'
import EmptyState from './EmptyState.vue'
const equipmentWarningData = ref([])
const loading = ref(true)
const minLoadingMs = 400
const statusMap = {
    0: { text: '在线', statusType: 'online' },
    1: { text: '离线', statusType: 'offline' },
    2: { text: '故障', statusType: 'fault' },
    3: { text: '报废', statusType: 'offline' }
}
const deviceTypeMap = {
    1: '便捷侦测箱',
    2: '反制枪',
    3: '察打一体'
}
const formatDeviceItem = (item) => {
    const statusInfo = statusMap[item?.status] || { text: '-', statusType: '' }
    return {
        id: item.id,
        name: item.deviceName || item.deviceModel || '-',
        status: statusInfo.text,
        statusType: statusInfo.statusType,
        type: deviceTypeMap[item.deviceType] || item.deviceType || '-',
        manufacturer: item.manufacturer || '-',
        azimuth: item.azimuth ?? 0,
        pitch: item.elevation ?? 0,
        range: item.effectiveRangeKm ?? 0,
        model: item.deviceModel || '-',
        longitude: item.longitude ?? '-',
        latitude: item.latitude ?? '-'
    }
}
const fetchDeviceWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, deviceStatusList: '1,2,3' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        equipmentWarningData.value = records.map(formatDeviceItem)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchDeviceWarning()
})
const onCardClick = (item) => {
    console.log('点击设备卡片:', item.id)
}
</script>
<style lang="scss" scoped>
.equipment-warning {
    width: 100%;
    height: 100%;
}
</style>
applications/task-work-order/src/views/dataCockpit/components/HistoryWarning.vue
New file
@@ -0,0 +1,123 @@
<template>
    <div
        class="history-warning"
        v-loading="loading"
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <!-- 搜索区(设计稿红框) -->
        <div class="search-box">
            <!-- 关键字 -->
            <el-input v-model="query.keyword" class="ztzf-data-cockpit-search-input" placeholder="请输入数据名称" clearable
                @keyup.enter="onSearch">
            </el-input>
            <!-- 日期范围 -->
            <el-date-picker v-model="query.dateRange"
                  class="ztzf-data-cockpit-date-picker"
                 popper-class="ztzf-data-cockpit-date-picker-popper"
                type="daterange"
                range-separator="~"
                start-placeholder="创建开始日期" end-placeholder="结束日期" format="YYYY年MM月DD日" value-format="YYYY-MM-DD"
                :clearable="true" @change="onSearch">
            </el-date-picker>
        </div>
        <!-- 列表 -->
        <div class="list-content">
            <EmptyState v-if="!filteredList.length" />
            <HistoryTemplate
                v-else
                v-for="(item, ind) in filteredList"
                :key="item.id || ind"
                :data="item"
                @click="onCardClick"
            />
        </div>
    </div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import HistoryTemplate from './templateComponents/HistoryTemplate.vue'
import { alarmLogApi } from '@/api/dataCockpit'
import EmptyState from './EmptyState.vue'
const historyWarningData = ref([])
const loading = ref(true)
const minLoadingMs = 400
// 查询条件:keyword + dateRange(YYYY-MM-DD数组)
const query = ref({
    keyword: '',
    dateRange: [] // [start, end]
})
const onSearch = () => {
    fetchHistoryWarning()
}
const onCardClick = (item) => {
    console.log('点击无人机卡片:', item.id)
}
const fetchHistoryWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const [startTime, endTime] = query.value.dateRange || []
        const keyword = (query.value.keyword || '').trim()
        const params = {
            current: 1,
            size: 20,
            alarmType: '2',
            startTime,
            endTime,
            droneName: keyword || undefined,
            droneSerialNo: keyword || undefined
        }
        const res = await alarmLogApi(params)
        const records = res?.data?.data?.records ?? []
        historyWarningData.value = records
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
const filteredList = computed(() => {
    return historyWarningData.value
})
onMounted(() => {
    fetchHistoryWarning()
})
</script>
<style scoped lang="scss">
.history-warning {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    .search-box {
        display: flex;
        flex-direction: column;
        gap: 10px;
        ::v-deep(.ztzf-data-cockpit-date-picker) {
            width: 100% !important;
        }
    }
    .list-content {
        height: 0;
        flex: 1;
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/LeftContainer.vue
New file
@@ -0,0 +1,219 @@
<template>
    <div class="left-container" :class="{ collapsed: collapsedModel }">
        <div class="wrapper">
            <TitleTemplate>异常情况告警</TitleTemplate>
            <div class="category-container">
                <div class="category-item" v-for="(item, ind) in categoryList" :key="ind"
                    @click="activeIdModel = item.id" :class="{ active: activeIdModel === item.id }">
                    {{ item.name }}
                    <span class="badge" v-if="item.badge">{{ item.badge }}</span>
                </div>
            </div>
            <div class="content">
                <component :is="currentComponent" />
            </div>
        </div>
        <div class="collapse-btn" @click="toggleCollapse">
            <span class="arrow" :class="collapsedModel ? 'right' : 'left'"></span>
        </div>
    </div>
</template>
<script setup>
import TitleTemplate from './templateComponents/TitleTemplate.vue'
import RealWarning from './RealWarning.vue'
import EquipmentWarning from './EquipmentWarning.vue'
import HistoryWarning from './HistoryWarning.vue'
import { alarmStatisticsApi } from '@/api/dataCockpit'
import { onMounted } from 'vue'
const props = defineProps({
    activeId: {
        type: Number,
        default: 1
    },
    collapsed: {
        type: Boolean,
        default: false
    }
})
const emit = defineEmits(['update:activeId', 'update:collapsed'])
const activeIdModel = computed({
    get: () => props.activeId,
    set: (val) => emit('update:activeId', val)
})
const collapsedModel = computed({
    get: () => props.collapsed,
    set: (val) => emit('update:collapsed', val)
})
const categoryList = ref([
    {
        id: 1,
        name: '实时告警',
        badge: 0,
        component: RealWarning
    },
    {
        id: 2,
        name: '设备告警',
        badge: 0,
        component: EquipmentWarning
    },
    {
        id: 3,
        name: '历史告警',
        badge: 0,
        component: HistoryWarning
    }
])
const currentComponent = computed(() => {
    return categoryList.value.find(i => i.id === activeIdModel.value)?.component
})
const toggleCollapse = () => {
    collapsedModel.value = !collapsedModel.value
}
const updateBadges = async () => {
    const res = await alarmStatisticsApi()
    const data = res?.data?.data || {}
    const realTotal = data.realTimeAlarmCount ?? 0
    const historyTotal = data.historyAlarmCount ?? 0
    const deviceTotal = data.deviceAlarmCount ?? 0
    categoryList.value = categoryList.value.map((item) => {
        if (item.id === 1) return { ...item, badge: realTotal }
        if (item.id === 2) return { ...item, badge: deviceTotal }
        if (item.id === 3) return { ...item, badge: historyTotal }
        return item
    })
}
onMounted(() => {
    updateBadges()
})
</script>
<style lang="scss" scoped>
.left-container {
    position: relative;
    transition: transform 0.3s ease-in-out;
    &.collapsed {
        transform: translateX(-100%);
    }
}
.collapse-btn {
    position: absolute;
    top: 50%;
    left: 357px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 10;
    transform: translateY(-50%);
    .arrow {
        width: 24px;
        height: 24px;
        background: url('@/assets/images/dataCockpit/left-btn.png') center / 100% 100% no-repeat;
    }
    &:hover {
        background: rgba(40, 79, 227, 0.6);
    }
}
.category-container {
    position: relative;
    display: flex;
    justify-content: space-between;
    &::after {
        content: '';
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        height: 1px;
        background: #333355;
    }
    .category-item {
        position: relative;
        width: 0;
        flex: 1;
        margin-left: 26px;
        height: 68px;
        line-height: 68px;
        font-family: Source Code Pro, Source Code Pro;
        font-weight: 400;
        font-size: 14px;
        color: #86909C;
        text-align: center;
        font-style: normal;
        text-transform: none;
        cursor: pointer;
        &:first-child {
            margin-left: 0;
        }
        &.active {
            font-weight: normal;
            color: #FFFFFF;
        }
        &::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 100%;
            height: 0;
        }
        &.active::after {
            height: 3px;
            background: #284FE3;
            box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
            border-radius: 5px 5px 0px 0px;
            z-index: 9;
        }
        .badge {
            position: absolute;
            top: 16px;
            right: 4px;
            min-width: 18px;
            line-height: 18px;
            font-size: 12px;
            color: #FFFFFF;
            background: #FF3B30;
            border-radius: 50%;
            text-align: center;
            box-shadow: 0px 2px 6px rgba(255, 59, 48, 0.6);
            pointer-events: none;
        }
    }
}
.content {
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
}
</style>
applications/task-work-order/src/views/dataCockpit/components/RealWarning.vue
New file
@@ -0,0 +1,109 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:29:07
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 14:49:09
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RealWarning.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-08 09:29:07
-->
<template>
    <div
        class="real-warning"
        v-loading="loading"
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <EmptyState v-if="!realWarningData.length" />
        <RealTemplate
            v-else
            v-for="(item, ind) in realWarningData"
            :key="ind"
            :data="item"
            @signal="() => handleAction('signal', item)"
            @counter="() => handleAction('counter', item)"
            @favorite="() => console.log('收藏', item.id)"
        />
    </div>
</template>
<script setup>
import RealTemplate from './templateComponents/RealTemplate.vue'
import { alarmLogApi, interferenceAndExpulsionApi } from '@/api/dataCockpit'
import { onMounted, ref } from 'vue'
import EmptyState from './EmptyState.vue'
import { ElMessage } from 'element-plus'
const realWarningData = ref([])
const loading = ref(true)
const minLoadingMs = 400
const actionTextMap = {
    signal: '信号干扰',
    counter: '诱导驱离'
}
const buildPayload = (item) => ({
    alarmRecordId: item.id,
    areaCode: item.areaCode,
    counterEffect: '',
    coverRadiusM: item.coverRadiusM,
    deployLatitude: item.latitude,
    deployLongitude: item.longitude,
    deviceCode: item.deviceCode,
    deviceId: item.deviceId,
    deviceModel: item.deviceModel,
    deviceName: item.deviceName,
    deviceType: item.deviceType,
    droneDeviceCode: item.droneSerialNo,
    droneName: item.droneName,
    droneType: item.droneType,
    findTime: item.alarmTime,
    id: item.id,
    workMode: ''
})
const handleAction = async (type, item) => {
    const actionText = actionTextMap[type] || '操作'
    try {
        const res = await interferenceAndExpulsionApi(buildPayload(item))
        if (res?.data?.success) {
            ElMessage({ type: 'success', message: `${actionText}成功` })
        } else {
            ElMessage({ type: 'error', message: res?.data?.msg || `${actionText}失败` })
        }
    } catch (error) {
        ElMessage({ type: 'error', message: `${actionText}失败` })
    }
}
const fetchRealWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, alarmType: '1' }
        const res = await alarmLogApi(params)
        const records = res?.data?.data?.records ?? []
        realWarningData.value = records
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchRealWarning()
})
</script>
<style lang="scss" scoped>
.real-warning {
    width: 100%;
    height: 100%;
}
</style>
applications/task-work-order/src/views/dataCockpit/components/RightContainer.vue
New file
@@ -0,0 +1,288 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-07 15:17:54
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 16:54:31
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-07 15:17:54
-->
<template>
    <div class="right-container" :class="{ collapsed: collapsedModel }">
        <div class="wrapper">
            <TitleTemplate>侦测反制设备</TitleTemplate>
            <div class="category-container">
                <div class="category-item" v-for="(item, ind) in categoryList" :key="ind">
                    <div class="category-item-logo">
                        <img :src="item.logo" alt="">
                    </div>
                    <div class="category-item-content">
                        <span class="name">{{ item.name }}</span>
                        <span class="val">{{ item.val }}</span>
                    </div>
                </div>
            </div>
            <div
                class="content"
                v-loading="loading"
                element-loading-background="rgba(5, 5, 15, 0.6)"
                element-loading-text="加载中..."
            >
                <EmptyState v-if="!counterDeviceStatusList.length" />
                <RealEquipmentTemplate
                    v-else
                    v-for="item in counterDeviceStatusList"
                    :key="item.id"
                    :data="item"
                    @history="onHistory"
                    @click="onCardClick"
                />
            </div>
        </div>
        <div class="collapse-btn" @click="toggleCollapse">
            <span class="arrow" :class="collapsedModel ? 'left' : 'right'"></span>
        </div>
        <DeviceHistoryDialog v-model="historyVisible" :device="historyDevice" />
    </div>
</template>
<script setup>
import TitleTemplate from './templateComponents/TitleTemplate.vue'
import RealEquipmentTemplate from './templateComponents/RealEquipmentTemplate.vue'
import DeviceHistoryDialog from './DeviceHistoryDialog.vue'
import EmptyState from './EmptyState.vue'
import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png'
import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png'
import { deviceSearchApi, deviceStatisticsApi } from '@/api/dataCockpit'
const props = defineProps({
    collapsed: {
        type: Boolean,
        default: false
    }
})
const emit = defineEmits(['update:collapsed', 'update:onlineDevices'])
const collapsedModel = computed({
    get: () => props.collapsed,
    set: (val) => emit('update:collapsed', val)
})
const categoryList = ref([
    {
        id: 1,
        name: '侦测设备',
        val: 0,
        logo: zcsbLogo
    },
    {
        id: 2,
        name: '反制设备',
        val: 0,
        logo: fzsbLogo
    }
])
const counterDeviceStatusList = ref([])
const historyVisible = ref(false)
const historyDevice = ref(null)
const loading = ref(true)
const minLoadingMs = 400
const onHistory = (item) => {
    historyDevice.value = item
    historyVisible.value = true
    console.log('历史数据:', item.id)
}
const onCardClick = (item) => {
    console.log('点击卡片:', item.id)
}
const toggleCollapse = () => {
    collapsedModel.value = !collapsedModel.value
}
const statusMap = {
    0: { text: '在线', actionType: 'detecting' },
    1: { text: '离线', actionType: '' },
    2: { text: '故障', actionType: 'jamming' },
    3: { text: '报废', actionType: '' }
}
const deviceTypeMap = {
    1: '便捷侦测箱',
    2: '反制枪',
    3: '察打一体'
}
const formatDeviceItem = (item) => {
    const statusInfo = statusMap[item?.status] || { text: '-', actionType: '' }
    return {
        id: item.id,
        name: item.deviceName || item.deviceModel || '-',
        actionText: statusInfo.text,
        actionType: statusInfo.actionType,
        type: deviceTypeMap[item.deviceType] || item.deviceType || '-',
        batteryPct: item.batteryPct ?? 0,
        azimuth: item.azimuth ?? 0,
        elevation: item.elevation ?? 0,
        status: item.status ?? '-',
        manufacturer: item.manufacturer || '-',
        detectTargetCnt: item.detectTargetCnt ?? 0,
        effectiveRangeKm: item.effectiveRangeKm ?? 0
    }
}
const fetchDeviceList = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, deviceStatusList: '0' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        counterDeviceStatusList.value = records.map(formatDeviceItem)
        emit('update:onlineDevices', records)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchDeviceList()
    fetchDeviceStatistics()
})
const normalizeType = (value) => {
    if (value === 1 || value === '1') return '侦测设备'
    if (value === 2 || value === '2') return '反制设备'
    if (value === 'detect') return '侦测设备'
    if (value === 'counter') return '反制设备'
    return value
}
const fetchDeviceStatistics = async () => {
    const res = await deviceStatisticsApi()
    const list = res?.data?.data ?? []
    const map = new Map(list.map((item) => [normalizeType(item.type), item.count]))
    categoryList.value = categoryList.value.map((item) => ({
        ...item,
        val: map.has(item.name) ? map.get(item.name) : item.val
    }))
}
</script>
<style lang="scss" scoped>
.category-container {
    margin-top: 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 28px;
    box-sizing: border-box;
    .category-item {
        position: relative;
        width: 0;
        flex: 1;
        display: flex;
        height: 64px;
        &-logo {
            display: flex;
            align-items: center;
            img {
                width: 40px;
                height: 40px;
            }
        }
        &-content {
            width: 0;
            flex: 1;
            display: flex;
            flex-direction: column;
            border-bottom: 1px solid #6C6C89;
            span {
                margin-left: 8px;
            }
            .name {
                font-family: Open Sans, Open Sans;
                font-weight: 400;
                font-size: 16px;
                color: #C3C3DD;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .val {
                margin-top: 10px;
                font-family: DIN, DIN;
                font-weight: bold;
                font-size: 22px;
                color: #FFFFFF;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
        }
    }
}
.content {
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
}
.right-container {
    transition: transform 0.3s ease-in-out;
    position: relative;
    &.collapsed {
        transform: translateX(100%);
    }
}
.collapse-btn {
    position: absolute;
    top: 50%;
    right: 357px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 10;
    transform: translateY(-50%);
    .arrow {
        width: 24px;
        height: 24px;
        background: url('@/assets/images/dataCockpit/right-btn.png') center / 100% 100% no-repeat;
    }
    &:hover {
        background: rgba(40, 79, 227, 0.6);
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue
New file
@@ -0,0 +1,171 @@
<template>
    <div class="device-card" @click="emit('click', data)">
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="data.statusType">
                    </span>
                    <span>
                        {{ data.status }}
                    </span>
                </span>
            </div>
        </div>
        <div class="content">
            <div class="row">
                <div class="col">
                    <span class="label">类型</span>
                    {{ data.type }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.manufacturer }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">方位角</span>
                    {{ data.azimuth }}°
                </div>
                <div class="col">
                    <span class="label">俯仰角</span>
                    {{ data.pitch }}°
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ data.range }}
                </div>
                <div class="col">
                    <span class="label">设备型号</span>
                    {{ data.model }}
                </div>
            </div>
            <div>
                <span class="label">部署位置</span>
                <span class="value">{{ data.longitude }},{{ data.latitude }}</span>
            </div>
        </div>
    </div>
</template>
<script setup>
const props = defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['click'])
</script>
<style scoped lang="scss">
.device-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.offline {
                    width: 4px;
                    height: 4px;
                    background: #939393;
                    border-radius: 50%;
                }
                span.fault {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            &>div {
                width: 0;
                flex: 1;
            }
        }
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue
New file
@@ -0,0 +1,149 @@
<template>
    <div class="drone-info-card" @click="emit('click', data)">
        <div class="title">
            <span class="name">
                {{ data.droneName }}
            </span>
        </div>
        <div class="rows">
            <div class="row">
                <span class="label">序列号</span>
                <span class="value">{{ data.droneSerialNo }}</span>
            </div>
            <div class="row">
                <span class="label">数据源</span>
                <span class="value">{{ getDataSource(data) }}</span>
            </div>
            <div class="row">
                <span class="label">信号频段</span>
                <span class="value">{{ formatFrequency(data.signalFreqMhz) }}</span>
            </div>
            <div class="row">
                <span class="label">发现时间</span>
                <span class="value">
                    <span class="date">{{ getAlarmDate(data.alarmTime) }}</span>
                    <span class="time">{{ getAlarmTime(data.alarmTime) }}</span>
                </span>
            </div>
            <div class="row">
                <span class="label">停留时间</span>
                <span class="value">{{ data.stayDuration }}</span>
            </div>
            <div class="row">
                <span class="label">反制方式</span>
                <span class="value">{{ getCounterWayText(data.counterWay) }}</span>
            </div>
            <div class="row">
                <span class="label">飞行状态</span>
                <span class="value">{{ getFlightStatusText(data.flightStatus) }}</span>
            </div>
        </div>
    </div>
</template>
<script setup>
defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['click'])
const getDataSource = (item) => {
    return item?.deviceName || item?.areaName || '-'
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHZ`
}
const getAlarmDate = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[0] || value
}
const getAlarmTime = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[1] || value
}
const getFlightStatusText = (value) => {
    if (value === 1 || value === '1') return '侦测中'
    if (value === 2 || value === '2') return '反制中'
    return value || '-'
}
const getCounterWayText = (value) => {
    if (value === 1 || value === '1') return '信号干扰'
    if (value === 2 || value === '2') return '诱导驱离'
    if (value === 3 || value === '3') return '无'
    return value || '-'
}
</script>
<style scoped lang="scss">
.drone-info-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:hover {
        filter: brightness(1.05);
    }
    .title {
        display: flex;
        align-items: center;
        gap: 6px;
        .name {
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: bold;
            font-size: 14px;
            color: #FFFFFF;
            text-align: left;
            font-style: normal;
            text-transform: none;
        }
    }
    .rows {
        margin-top: 10px;
        display: flex;
        flex-direction: column;
        gap: 10px;
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        .row {
            line-height: 22px;
            .label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue
New file
@@ -0,0 +1,213 @@
<template>
    <div class="counter-card" @click="emit('click', data)">
        <!-- 头部 -->
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="getStatusType(data.status)">
                    </span>
                    <span>
                        {{ getStatusText(data.status) }}
                    </span>
                </span>
            </div>
        </div>
        <!-- 字段区:两列 -->
        <div class="content">
            <div class="row">
                <div class="col">
                    <span class="label">类型</span>
                    {{ data.type }}
                </div>
                <div class="col">
                    <span class="label">电量</span>
                    {{ data.batteryPct }}%
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">方位角</span>
                    {{ data.azimuth }}°
                </div>
                <div class="col">
                    <span class="label">俯仰角</span>
                    {{ data.elevation }}°
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">状态</span>
                    {{ getStatusText(data.status) }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.manufacturer }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">侦测目标</span>
                    {{ data.detectTargetCnt }}台
                </div>
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ data.effectiveRangeKm }}KM
                </div>
            </div>
        </div>
        <!-- 底部按钮 -->
        <div class="footer">
            <el-button color="#284FE3" type="primary" @click.stop="emit('history', data)">历史数据</el-button>
        </div>
    </div>
</template>
<script setup>
defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['history', 'click'])
const getStatusText = (value) => {
    if (value === 0 || value === '0') return '在线'
    if (value === 1 || value === '1') return '离线'
    if (value === 2 || value === '2') return '故障'
    if (value === 3 || value === '3') return '报废'
    return value || '-'
}
const getStatusType = (value) => {
    if (value === 0 || value === '0') return 'detecting'
    if (value === 2 || value === '2') return 'jamming'
    return ''
}
</script>
<style scoped lang="scss">
.counter-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.detecting {
                    width: 4px;
                    height: 4px;
                    background: #18FF4A;
                    border-radius: 50%;
                }
                span.jamming {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            &>div {
                width: 0;
                flex: 1;
            }
        }
    }
    .footer {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        margin-top: 10px;
        ::v-deep(.el-button) {
            width: 96px;
            height: 36px;
        }
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/templateComponents/RealTemplate.vue
New file
@@ -0,0 +1,245 @@
<template>
    <div class="drone-card">
        <div class="header">
            <div class="title">
                <span class="name">{{ data.droneName }}</span>
                <span class="status">
                    <span :class="getStatusType(data)">
                    </span>
                    <span>
                        {{ getStatusText(data) }}
                    </span>
                </span>
            </div>
            <div class="actions">
                <span class="star" @click="emit('favorite')">
                    {{ data.isFavorite ? '★' : '☆' }}
                </span>
            </div>
        </div>
        <div class="content">
            <div>
                <span class="label">序列号</span>
                {{ data.droneSerialNo }}
            </div>
            <div>
                <span class="label">数据源</span>
                {{ getDataSource(data) }}
            </div>
            <div>
                <span class="label">经纬度</span>
                {{ data.longitude }}, {{ data.latitude }}
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">信号频段</span>
                    {{ formatFrequency(data.signalFreqMhz) }}
                </div>
                <div class="col">
                    <span class="label">发现时间</span>
                    {{ getAlarmTime(data.alarmTime) }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">飞行高度</span>
                    {{ formatHeight(data.flightHeightM) }}
                </div>
                <div class="col">
                    <span class="label">飞行速度</span>
                    {{ formatSpeed(data.flightSpeedMs) }}
                </div>
            </div>
        </div>
        <div class="footer">
            <el-button class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button>
            <el-button color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button>
        </div>
    </div>
</template>
<script setup>
const props = defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['signal', 'counter', 'favorite'])
const getStatusText = (item) => {
    const status = item?.flightStatus
    if (status === 1 || status === '1') return '侦测中'
    if (status === 2 || status === '2') return '反制中'
    return status || '侦测中'
}
const getStatusType = (item) => {
    const statusText = getStatusText(item)
    const counterWay = item?.counterWay
    if (statusText.includes('反制') || counterWay === 2 || counterWay === '2' || counterWay === '诱导驱离') {
        return 'countering'
    }
    return 'detecting'
}
const getDataSource = (item) => {
    return item?.deviceName || item?.areaName || '-'
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHz`
}
const getAlarmTime = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[1] || value
}
const formatHeight = (value) => {
    if (value == null || value === '') return '-'
    return `${value}m`
}
const formatSpeed = (value) => {
    if (value == null || value === '') return '-'
    return `${value}m/s`
}
</script>
<style scoped lang="scss">
.drone-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:hover {
        background: #29294D;
        ;
    }
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.detecting {
                    width: 4px;
                    height: 4px;
                    background: #18FF4A;
                    border-radius: 50%;
                }
                span.countering {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
        .actions {
            display: flex;
            gap: 8px;
            font-size: 16px;
            cursor: pointer;
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            justify-content: space-between;
        }
    }
    .footer {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        margin-top: 10px;
        ::v-deep(.el-button) {
            width: 96px;
            height: 36px;
            &.general.el-button {
                border-radius: 4px 4px 4px 4px;
                border: 1px solid rgba(255, 255, 255, 0.5);
            }
        }
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/components/templateComponents/TitleTemplate.vue
New file
@@ -0,0 +1,43 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:32:52
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 09:43:00
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\templateComponents\TitleTemplate.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-08 09:32:52
-->
<template>
    <div class="title-box">
        <div class="title">
            <slot></slot>
        </div>
    </div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.title-box {
    padding-left: 26px;
    background: url('@/assets/images/dataCockpit/title-bg.png') center / 100% 100% no-repeat;
    height: 34px;
    .title {
        height: 25px;
        line-height: 25px;
        font-family: PangMen;
        font-weight: 400;
        font-size: 18px;
        color: #FFFFFF;
        letter-spacing: 1px;
        text-align: left;
        font-style: normal;
        text-transform: none;
    }
}
</style>
applications/task-work-order/src/views/dataCockpit/index.vue
New file
@@ -0,0 +1,96 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 16:35:50
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 16:34:39
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\index.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 16:35:50
-->
<template>
  <div class="page-container">
    <DeviceMapContainer
      :online-devices="onlineDevices"
      :left-collapsed="leftCollapsed"
      container-id="data-cockpit-map"
    />
    <LeftContainer class="left-container" v-model:activeId="leftActiveId" v-model:collapsed="leftCollapsed" />
    <RightContainer class="right-container" v-model:collapsed="rightCollapsed" v-model:onlineDevices="onlineDevices" />
    <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" />
  </div>
</template>
<script setup>
  import DeviceMapContainer from '@/components/map-container/device-map-container.vue';
  import LeftContainer from './components/LeftContainer.vue';
  import RightContainer from './components/RightContainer.vue';
  import CenterContainer from './components/CenterContainer.vue';
  const leftActiveId = ref(1);
  const leftCollapsed = ref(false);
  const rightCollapsed = ref(false);
  const onlineDevices = ref([]);
  const onSelectWarning = (type) => {
    leftActiveId.value = type;
    leftCollapsed.value = false;
  };
  const onSelectEquipment = () => {
    rightCollapsed.value = false;
  };
</script>
<style scoped lang="scss">
  .page-container {
    position: relative;
    width: 100%;
    height: 100%;
  }
  .left-container,
  .right-container {
    position: absolute;
    top: 96px;
    bottom: 0;
    width: 401px;
    z-index: 9;
    backdrop-filter: blur(2px);
    ::v-deep(.wrapper) {
      display: flex;
      flex-direction: column;
      position: absolute;
      top: 17px;
      bottom: 20px;
      padding: 20px;
      width: 300px;
      background: rgba(0,0,0,0.3);
      backdrop-filter: blur(16px);
      border-radius: 10px 10px 10px 10px;
    }
  }
  .left-container {
    top: 96px;
    left: 0;
    background: linear-gradient( 270deg, rgba(17,23,34,0) 0%, rgba(17,23,34,0.56) 50%, rgba(17,23,34,0.96) 100%);
    ::v-deep(.wrapper) {
      left: 17px;
    }
  }
  .right-container {
    top: 54px;
    right: 0;
    background: linear-gradient( 270deg, rgba(17,23,34,0.96) 0.16%, rgba(17,23,34,0.56) 57.57%, rgba(4,30,37,0) 100%);
    ::v-deep(.wrapper) {
      top: 21px;
      right: 17px;
    }
  }
</style>
applications/task-work-order/src/views/desk/notice.vue
File was deleted
applications/task-work-order/src/views/device/addDevice.vue
File was deleted
applications/task-work-order/src/views/device/airport.vue
File was deleted
applications/task-work-order/src/views/device/components/DeviceSettingBox.vue
File was deleted
applications/task-work-order/src/views/device/components/DeviceSettingPopover.vue
File was deleted
applications/task-work-order/src/views/device/components/DockControlPanel.vue
File was deleted
applications/task-work-order/src/views/device/components/devicePerShare.vue
File was deleted
applications/task-work-order/src/views/device/components/firmwareManage.vue
File was deleted
applications/task-work-order/src/views/device/components/use-device-setting.js
File was deleted
applications/task-work-order/src/views/device/components/use-dock-control.js
File was deleted
applications/task-work-order/src/views/device/devicePerMenu.vue
File was deleted
applications/task-work-order/src/views/device/fly.vue
File was deleted
applications/task-work-order/src/views/device/index.vue
File was deleted
applications/task-work-order/src/views/flow/deploy.vue
File was deleted
applications/task-work-order/src/views/flow/follow.vue
File was deleted
applications/task-work-order/src/views/flow/manager.vue
File was deleted
applications/task-work-order/src/views/flow/model.vue
File was deleted
applications/task-work-order/src/views/job/components/CancelTaskDialog.vue
File was deleted
applications/task-work-order/src/views/job/components/DeviceJobDetails.vue
File was deleted
applications/task-work-order/src/views/job/components/DeviceJobDetailsMap.vue
File was deleted
applications/task-work-order/src/views/job/components/FileTypePlay.vue
File was deleted
applications/task-work-order/src/views/job/components/JobRelatedEvents.vue
File was deleted
applications/task-work-order/src/views/job/components/SearchBox.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskAlgorithmBusiness.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskIntermediateContent/AddTask.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskIntermediateContent/TaskTable.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskTop/TaskEvent.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskTop/TaskIndustry.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskTop/TaskTime.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskTop/TaskTop.vue
File was deleted
applications/task-work-order/src/views/job/components/TaskTop/TaskTotal.vue
File was deleted
applications/task-work-order/src/views/job/components/executeNestDetails.vue
File was deleted
applications/task-work-order/src/views/job/const/drc.js
File was deleted
applications/task-work-order/src/views/job/jobinfo.vue
File was deleted
applications/task-work-order/src/views/job/jobserver.vue
File was deleted
applications/task-work-order/src/views/job/jobstatistics.vue
File was deleted
applications/task-work-order/src/views/layerManagement/components/folderFile.vue
File was deleted
applications/task-work-order/src/views/layerManagement/components/leftList.vue
File was deleted
applications/task-work-order/src/views/layerManagement/components/nationalSpatialPlanning.vue
File was deleted
applications/task-work-order/src/views/layerManagement/components/rightEdit.vue
File was deleted
applications/task-work-order/src/views/layerManagement/index.vue
File was deleted
applications/task-work-order/src/views/license/license.vue
File was deleted
applications/task-work-order/src/views/license/upload.vue
File was deleted
applications/task-work-order/src/views/monitor/log/api.vue
File was deleted
applications/task-work-order/src/views/monitor/log/error.vue
File was deleted
applications/task-work-order/src/views/monitor/log/flightLog.vue
File was deleted
applications/task-work-order/src/views/monitor/log/usual.vue
File was deleted
applications/task-work-order/src/views/odm/task.vue
File was deleted
applications/task-work-order/src/views/report/reportlist.vue
File was deleted
applications/task-work-order/src/views/resource/components/spotDetails.vue
@@ -168,7 +168,7 @@
const viewInstance = shallowRef(null);
const homeViewer = shallowRef(null);
let tbJwdList = [];
const loading = ref(false);
const loading = ref(true);
const tableLoading = ref(true);
const tableData = ref([]);
const AlltableData = ref([]);
applications/task-work-order/src/views/system/menu.vue
@@ -202,6 +202,10 @@
                label: '新大屏',
                value: 4,
              },
              {
                label: '无人机管控',
                value: 5,
              },
            ],
            hide: true,
            rules: [
applications/task-work-order/src/views/system/user/FormDiaLog.vue
New file
@@ -0,0 +1,451 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="800px">
        <!-- 查看模式 -->
        <div v-if="readonly" class="view-mode">
            <el-row :gutter="16">
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">登录账号:</span>
                        {{ formData.account }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">用户姓名:</span>
                        {{ formData.realName }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">手机号码:</span>
                        {{ formData.phone }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">电子邮箱:</span>
                        {{ formData.email }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">用户性别:</span>
                        {{ getSexLabel(formData.sex) }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">用户生日:</span>
                        {{ formData.birthday }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">所属角色:</span>
                        {{ formData.roleName }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">所属部门:</span>
                        {{ formData.deptName }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">用户编号:</span>
                        {{ formData.code }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">账号状态:</span>
                        {{ formData.statusName }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">到期时间:</span>
                        {{ formData.expireTime }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">次数限制:</span>
                        {{ formData.maxLoginNum }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">飞行开始时间:</span>
                        {{ formData.flightStartTime }}
                    </div>
                </el-col>
                <el-col :span="12">
                    <div class="view-item">
                        <span class="label">飞行结束时间:</span>
                        {{ formData.flightEndTime }}
                    </div>
                </el-col>
            </el-row>
        </div>
        <!-- 编辑/新增模式 -->
        <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="110px">
            <el-divider content-position="left">基础信息</el-divider>
            <el-row :gutter="16">
                <el-col :span="12">
                    <el-form-item label="登录账号" prop="account">
                        <el-input v-model="formData.account" maxlength="50" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="dialogMode === 'add'">
                    <el-form-item label="密码" prop="password">
                        <el-input
                            v-model="formData.password"
                            type="password"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                            show-password
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12" v-if="dialogMode === 'add'">
                    <el-form-item label="确认密码" prop="password2">
                        <el-input
                            v-model="formData.password2"
                            type="password"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                            show-password
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-divider content-position="left">详细信息</el-divider>
            <el-row :gutter="16">
                <el-col :span="12">
                    <el-form-item label="用户姓名" prop="realName">
                        <el-input v-model="formData.realName" maxlength="50" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="手机号码" prop="phone">
                        <el-input v-model="formData.phone" maxlength="20" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="电子邮箱" prop="email">
                        <el-input v-model="formData.email" maxlength="50" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="用户性别" prop="sex">
                        <el-select v-model="formData.sex" placeholder="请选择" clearable>
                            <el-option label="男" :value="1" />
                            <el-option label="女" :value="2" />
                            <el-option label="未知" :value="3" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="用户生日" prop="birthday">
                        <el-date-picker
                            v-model="formData.birthday"
                            type="datetime"
                            placeholder="请选择"
                            format="YYYY-MM-DD HH:mm:ss"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            clearable
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-divider content-position="left">职责信息</el-divider>
            <el-row :gutter="16">
                <el-col :span="12">
                    <el-form-item label="用户编号" prop="code">
                        <el-input v-model="formData.code" maxlength="50" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="所属角色" prop="roleId">
                        <el-tree-select
                            v-model="formData.roleId"
                            :data="roleTree"
                            :props="treeProps"
                            node-key="id"
                            check-strictly
                            clearable
                            placeholder="请选择"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="所属部门" prop="deptId">
                        <el-tree-select
                            v-model="formData.deptId"
                            :data="deptTree"
                            :props="deptTreeSelectProps"
                            node-key="id"
                            check-strictly
                            clearable
                            placeholder="请选择"
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-divider content-position="left">登录限制</el-divider>
            <el-row :gutter="16">
                <el-col :span="12">
                    <el-form-item label="到期时间" prop="expireTime">
                        <el-date-picker
                            v-model="formData.expireTime"
                            type="datetime"
                            placeholder="请选择"
                            format="YYYY-MM-DD HH:mm:ss"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="次数限制" prop="maxLoginNum">
                        <el-input v-model="formData.maxLoginNum" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="飞行开始时间" prop="flightStartTime">
                        <el-time-picker
                            v-model="formData.flightStartTime"
                            placeholder="请选择"
                            format="HH:mm"
                            value-format="HH:mm"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="飞行结束时间" prop="flightEndTime">
                        <el-time-picker
                            v-model="formData.flightEndTime"
                            placeholder="请选择"
                            format="HH:mm"
                            value-format="HH:mm"
                            clearable
                        />
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <template #footer>
            <el-button @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">
                确定
            </el-button>
        </template>
    </el-dialog>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getUser, add, update } from '@/api/system/user'
import { getDeptTree } from '@/api/system/dept'
import { getRoleTree } from '@/api/system/role'
import func from '@/utils/func'
// 初始化表单
const initForm = () => ({
    account: '',
    password: '',
    password2: '',
    realName: '',
    phone: '',
    email: '',
    sex: null,
    birthday: '',
    code: '',
    roleId: '',
    deptId: '',
    expireTime: '',
    maxLoginNum: '',
    flightStartTime: '',
    flightEndTime: '',
})
const treeProps = {
    label: 'title',
    children: 'children',
}
const deptTreeSelectProps = {
    label: 'name',
    value: 'id',
    children: 'children',
}
const emit = defineEmits(['success'])
const formRef = ref(null)
const formData = ref(initForm())
const visible = defineModel()
const dialogMode = ref('add')
const submitting = ref(false)
const readonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑用户', view: '查看用户', add: '新增用户' })
const roleTree = ref([])
const deptTree = ref([])
// 密码验证
const validatePass = (rule, value, callback) => {
    if (value === '') {
        callback(new Error('请输入密码'))
    } else {
        callback()
    }
}
const validatePass2 = (rule, value, callback) => {
    if (value === '') {
        callback(new Error('请再次输入密码'))
    } else if (value !== formData.value.password) {
        callback(new Error('两次输入密码不一致!'))
    } else {
        callback()
    }
}
const rules = {
    account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
    password: [{ required: true, validator: validatePass, trigger: 'blur' }],
    password2: [{ required: true, validator: validatePass2, trigger: 'blur' }],
    realName: [
        { required: true, message: '请输入用户姓名', trigger: 'blur' },
        { min: 2, max: 50, message: '姓名长度在2到50个字符', trigger: 'blur' },
    ],
    roleId: [{ required: true, message: '请选择所属角色', trigger: 'change' }],
    deptId: [{ required: true, message: '请选择所属部门', trigger: 'change' }],
}
// 获取性别标签
function getSexLabel(sex) {
    const map = { 1: '男', 2: '女', 3: '未知' }
    return map[sex] || ''
}
// 获取角色树
async function getRoleTreeData() {
    const res = await getRoleTree('000000')
    roleTree.value = res.data.data
}
// 获取部门树
async function getDeptTreeData() {
    const res = await getDeptTree('000000')
    deptTree.value = res.data.data
}
// 关闭弹框
function handleCancel() {
    visible.value = false
}
// 验证飞行时间
function validateFlightTime() {
    if (formData.value.flightStartTime && formData.value.flightEndTime) {
        const [startHours, startMinutes] = formData.value.flightStartTime.split(':').map(Number)
        const [endHours, endMinutes] = formData.value.flightEndTime.split(':').map(Number)
        const startTotalMinutes = startHours * 60 + startMinutes
        const endTotalMinutes = endHours * 60 + endMinutes
        if (endTotalMinutes < startTotalMinutes) {
            ElMessage.error('飞行结束时间不能小于飞行开始时间')
            return false
        }
    }
    return true
}
// 提交新增/编辑
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    if (!validateFlightTime()) return
    submitting.value = true
    try {
        const submitData = { ...formData.value }
        submitData.roleId = func.toStr(submitData.roleId)
        submitData.name = submitData.realName
        if (dialogMode.value === 'add') {
            await add(submitData)
            ElMessage.success('新增成功')
        } else {
            await update(submitData)
            ElMessage.success('更新成功')
        }
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
}
// 加载详情
async function loadDetail() {
    if (!formData.value.id) return
    const res = await getUser(formData.value.id)
    const data = res?.data?.data ?? {}
    // 处理 roleId
    if (data.roleId) {
        data.roleId = func.split(data.roleId)[0] || data.roleId
    }
    formData.value = data
}
// 打开弹框
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    if (dialogMode.value === 'add') {
        formData.value = initForm()
    } else {
        formData.value = { id: row.id }
        await loadDetail()
    }
}
onMounted(() => {
    getRoleTreeData()
    getDeptTreeData()
})
defineExpose({ open })
</script>
<style scoped lang="scss">
.view-mode {
    .view-item {
        padding: 10px 0;
        .label {
            color: #606266;
            font-weight: 500;
        }
    }
}
:deep(.el-select) {
    width: 100%;
}
:deep(.el-date-editor) {
    width: 100%;
}
:deep(.el-tree-select) {
    width: 100%;
}
</style>
applications/task-work-order/src/views/system/user/index.vue
New file
@@ -0,0 +1,504 @@
<template>
    <basic-container>
        <!-- 左侧部门树 -->
        <el-row :gutter="16" class="user-container">
            <el-col :span="4">
                <div class="tree-box">
                    <el-scrollbar>
                        <el-tree
                            :data="deptTree"
                            :props="deptTreeProps"
                            node-key="id"
                            :load="loadDeptTree"
                            lazy
                            @node-click="handleDeptNodeClick"
                        />
                    </el-scrollbar>
                </div>
            </el-col>
            <!-- 右侧列表区 -->
            <el-col :span="20">
                <!-- 搜索表单 -->
                <el-form ref="queryParamsRef" :model="searchParams">
                    <el-row :gutter="16">
                        <el-col :span="5">
                            <el-form-item label="登录账号" prop="account">
                                <el-input v-model="searchParams.account" placeholder="请输入" clearable @clear="handleSearch" />
                            </el-form-item>
                        </el-col>
                        <el-col :span="5">
                            <el-form-item label="用户姓名" prop="realName">
                                <el-input v-model="searchParams.realName" placeholder="请输入" clearable @clear="handleSearch" />
                            </el-form-item>
                        </el-col>
                        <el-col :span="4">
                            <el-form-item>
                                <el-button @click="resetForm">重置</el-button>
                                <el-button type="primary" @click="handleSearch">查询</el-button>
                            </el-form-item>
                        </el-col>
                    </el-row>
                </el-form>
                <!-- 操作按钮 -->
                <div class="btn-group">
                    <el-button v-if="permission.user_add && !auditMode" type="primary" @click="openForm('add')">新增</el-button>
                    <el-button
                        v-if="permission.user_delete && !auditMode"
                        type="danger"
                        :disabled="!selectedIds.length"
                        @click="handleDelete()"
                    >
                        删除
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && auditMode"
                        type="success"
                        :disabled="!selectedIds.length"
                        @click="handleAuditPass"
                    >
                        通过
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && auditMode"
                        type="danger"
                        :disabled="!selectedIds.length"
                        @click="handleAuditRefuse"
                    >
                        拒绝
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && auditMode"
                        type="primary"
                        plain
                        @click="handleAuditBack"
                    >
                        返回
                    </el-button>
                    <el-button
                        v-if="permission.user_role && !auditMode"
                        type="info"
                        plain
                        :disabled="!selectedIds.length"
                        @click="handleGrant"
                    >
                        角色配置
                    </el-button>
                    <el-button
                        v-if="permission.user_reset && !auditMode"
                        type="info"
                        plain
                        :disabled="!selectedIds.length"
                        @click="handleReset"
                    >
                        密码重置
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && !auditMode"
                        type="info"
                        plain
                        :disabled="!selectedIds.length"
                        @click="handleLock"
                    >
                        账号解封
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && !auditMode"
                        type="success"
                        plain
                        @click="handleImport"
                    >
                        导入
                    </el-button>
                    <el-button
                        v-if="userInfo.role_name?.includes('admin') && !auditMode"
                        type="warning"
                        plain
                        @click="handleExport"
                    >
                        导出
                    </el-button>
                </div>
                <!-- 用户列表 -->
                <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="55" />
                    <el-table-column type="index" width="60" label="序号" />
                    <el-table-column prop="account" label="登录账号" />
                    <el-table-column prop="realName" label="用户姓名" />
                    <el-table-column prop="roleName" label="所属角色">
                        <template v-slot="{ row }">
                            <el-tag v-if="row.roleName">{{ row.roleName }}</el-tag>
                        </template>
                    </el-table-column>
                    <el-table-column prop="deptName" label="所属部门">
                        <template v-slot="{ row }">
                            <el-tag v-if="row.deptName">{{ row.deptName }}</el-tag>
                        </template>
                    </el-table-column>
                    <el-table-column prop="phone" label="手机号码" />
                    <el-table-column label="操作" width="180">
                        <template v-slot="{ row }">
                            <el-link v-if="permission.user_view" @click="openForm('view', row)" type="primary">查看</el-link>
                            <el-link v-if="permission.user_edit && !auditMode" @click="openForm('edit', row)" type="warning">
                                编辑
                            </el-link>
                            <el-link v-if="permission.user_delete && !auditMode" @click="handleDelete(row)" type="danger">
                                删除
                            </el-link>
                        </template>
                    </el-table-column>
                </el-table>
                <!-- 分页 -->
                <div>
                    <el-pagination
                        v-model:current-page="searchParams.current"
                        v-model:page-size="searchParams.size"
                        :total="total"
                        @change="getList"
                    />
                </div>
            </el-col>
        </el-row>
        <!-- 用户表单弹框 -->
        <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" />
        <!-- 角色配置弹框 -->
        <el-dialog title="用户角色配置" append-to-body v-model="roleBox" width="345px">
            <el-tree
                :data="roleGrantList"
                show-checkbox
                check-strictly
                default-expand-all
                node-key="id"
                ref="treeRoleRef"
                :default-checked-keys="roleTreeObj"
                :props="roleProps"
            ></el-tree>
            <template #footer>
                <span class="dialog-footer">
                    <el-button @click="roleBox = false">取 消</el-button>
                    <el-button type="primary" @click="submitRole">确 定</el-button>
                </span>
            </template>
        </el-dialog>
        <!-- 导入弹框 -->
        <el-dialog title="用户数据导入" append-to-body v-model="excelBox" width="555px">
            <el-form :model="excelForm" label-width="100px">
                <el-form-item label="数据覆盖">
                    <el-switch v-model="excelForm.isCovered" :active-value="1" :inactive-value="0" />
                </el-form-item>
                <el-form-item label="模板上传">
                    <el-upload
                        :action="uploadAction"
                        :on-success="handleUploadSuccess"
                        accept=".xls,.xlsx"
                        :show-file-list="false"
                    >
                        <el-button type="primary">点击上传</el-button>
                    </el-upload>
                    <div class="upload-tip">请上传 .xls,.xlsx 标准格式文件</div>
                </el-form-item>
                <el-form-item label="模板下载">
                    <el-button type="primary" @click="handleTemplate">点击下载</el-button>
                </el-form-item>
            </el-form>
        </el-dialog>
    </basic-container>
</template>
<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useStore } from 'vuex'
import FormDiaLog from './FormDiaLog.vue'
import { getList as getListApi, remove, grant, resetPassword, unlock, auditPass, auditRefuse } from '@/api/system/user'
import { exportBlob } from '@/api/common'
import { getDeptTree, getDeptLazyTree } from '@/api/system/dept'
import { getRoleTree } from '@/api/system/role'
import website from '@/config/website'
import { getToken } from '@/utils/auth'
import { downloadXls } from '@/utils/util'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import func from '@/utils/func'
const store = useStore()
const userInfo = computed(() => store.getters.userInfo)
const permission = computed(() => store.getters.permission)
// 初始化搜索参数
const initSearchParams = () => ({
    account: '',
    realName: '',
    current: 1,
    size: 10,
})
const searchParams = ref(initSearchParams())
const total = ref(0)
const loading = ref(false)
const list = ref([])
const selectedIds = ref([])
const selectionList = ref([])
const queryParamsRef = ref(null)
const dialogRef = ref(null)
const dialogVisible = ref(false)
const auditMode = ref(false)
const treeDeptId = ref('')
// 部门树
const deptTree = ref([])
const deptTreeProps = {
    label: 'title',
    children: 'children',
    isLeaf: 'leaf',
}
// 角色配置
const roleBox = ref(false)
const roleGrantList = ref([])
const roleTreeObj = ref([])
const treeRoleRef = ref(null)
const roleProps = {
    label: 'title',
    value: 'key',
}
// 导入相关
const excelBox = ref(false)
const excelForm = ref({
    isCovered: 0,
})
const uploadAction = computed(() => `/blade-system/user/import-user?isCovered=${excelForm.value.isCovered}`)
provide('treeDeptId', treeDeptId)
// 加载部门树节点
async function loadDeptTree(node, resolve) {
    const parentId = node.level === 0 ? 0 : node.data.id
    const param = {
        parentId: parentId,
        level: node.level,
    }
    const res = await getDeptLazyTree(param)
    resolve(
        res.data.data.map(item => ({
            ...item,
            leaf: !item.hasChildren,
        }))
    )
}
// 部门节点点击
function handleDeptNodeClick(data) {
    treeDeptId.value = data.id
    searchParams.value.current = 1
    getList()
}
// 获取列表
async function getList() {
    loading.value = true
    try {
        const res = await getListApi(
            searchParams.value.current,
            searchParams.value.size,
            {
                account: searchParams.value.account,
                realName: searchParams.value.realName,
                status: auditMode.value ? 0 : 1,
            },
            treeDeptId.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 = initSearchParams()
    treeDeptId.value = ''
    getList()
}
// 删除
async function handleDelete(row) {
    const tips = row ? '该条' : '选中的项'
    await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', { type: 'warning' })
    const ids = row ? row.id : selectedIds.value.join(',')
    await remove(ids)
    ElMessage.success('删除成功')
    selectedIds.value = []
    getList()
}
// 勾选值设置
function handleSelectionChange(rows) {
    selectionList.value = rows
    selectedIds.value = rows.map(item => item.id)
}
// 角色配置
async function handleGrant() {
    if (selectedIds.value.length === 0) {
        ElMessage.warning('请选择至少一条数据')
        return
    }
    roleTreeObj.value = []
    if (selectionList.value.length === 1) {
        roleTreeObj.value = selectionList.value[0].roleId?.split(',') || []
    }
    const res = await getRoleTree()
    roleGrantList.value = res.data.data
    roleBox.value = true
}
// 提交角色配置
async function submitRole() {
    const roleList = treeRoleRef.value.getCheckedKeys().join(',')
    await grant(selectedIds.value.join(','), roleList)
    roleBox.value = false
    ElMessage.success('操作成功')
    getList()
}
// 密码重置
async function handleReset() {
    if (selectedIds.value.length === 0) {
        ElMessage.warning('请选择至少一条数据')
        return
    }
    await ElMessageBox.confirm('确定将选择账号密码重置为123456?', '提示', { type: 'warning' })
    await resetPassword(selectedIds.value.join(','))
    ElMessage.success('操作成功')
}
// 账号解封
async function handleLock() {
    if (selectedIds.value.length === 0) {
        ElMessage.warning('请选择至少一条数据')
        return
    }
    await ElMessageBox.confirm('确定将选择账号解封?', '提示', { type: 'warning' })
    await unlock(selectedIds.value.join(','))
    ElMessage.success('操作成功')
}
// 审核通过
async function handleAuditPass() {
    if (selectedIds.value.length === 0) {
        ElMessage.warning('请选择至少一条数据')
        return
    }
    await ElMessageBox.confirm('确定将选择数据通过审核?', '提示', { type: 'warning' })
    await auditPass(selectedIds.value.join(','))
    ElMessage.success('操作成功')
    getList()
}
// 审核拒绝
async function handleAuditRefuse() {
    if (selectedIds.value.length === 0) {
        ElMessage.warning('请选择至少一条数据')
        return
    }
    await ElMessageBox.confirm('确定将选择数据拒绝审核?', '提示', { type: 'warning' })
    await auditRefuse(selectedIds.value.join(','))
    ElMessage.success('操作成功')
    getList()
}
// 返回正常模式
function handleAuditBack() {
    auditMode.value = false
    getList()
}
// 导入
function handleImport() {
    excelBox.value = true
}
// 上传成功
function handleUploadSuccess() {
    excelBox.value = false
    getList()
    ElMessage.success('导入成功')
}
// 导出
function handleExport() {
    const account = func.toStr(searchParams.value.account)
    const realName = func.toStr(searchParams.value.realName)
    ElMessageBox.confirm('是否导出用户数据?', '提示', { type: 'warning' }).then(() => {
        NProgress.start()
        exportBlob(
            `/blade-system/user/export-user?${website.tokenHeader}=${getToken()}&account=${account}&realName=${realName}`
        ).then(res => {
            downloadXls(res.data, `用户数据表${new Date().toISOString().slice(0, 10)}.xlsx`)
            NProgress.done()
        })
    })
}
// 下载模板
function handleTemplate() {
    exportBlob(`/blade-system/user/export-template?${website.tokenHeader}=${getToken()}`).then(res => {
        downloadXls(res.data, '用户数据模板.xlsx')
    })
}
// 新增/编辑/查看 弹框
function openForm(mode, row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode, row })
    })
}
onMounted(() => {
    getList()
})
</script>
<style scoped lang="scss">
.user-container {
    height: 100%;
}
.tree-box {
    height: 100%;
    padding: 10px;
    border-radius: 5px;
    background-color: #fff;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    :deep(.el-scrollbar) {
        height: 100%;
    }
}
.btn-group {
    margin-bottom: 16px;
}
.upload-tip {
    color: #999;
    font-size: 12px;
    margin-top: 8px;
}
</style>
applications/task-work-order/src/views/test.vue
File was deleted
applications/task-work-order/src/views/tickets/component/AddEditDetails.vue
File was deleted
applications/task-work-order/src/views/tickets/component/SearchBox.vue
File was deleted
applications/task-work-order/src/views/tickets/orderLog.vue
File was deleted
applications/task-work-order/src/views/tickets/orderLogCopy.vue
File was deleted
applications/task-work-order/src/views/tickets/ticket-copy.vue
File was deleted
applications/task-work-order/src/views/tickets/ticket.vue
File was deleted
applications/task-work-order/src/views/tickets/ticketComponent/CreateTicketDialog.vue
File was deleted
applications/task-work-order/src/views/tickets/ticketComponent/DispatchDialog.vue
File was deleted
applications/task-work-order/src/views/tickets/ticketComponent/RecheckDialog.vue
File was deleted
applications/task-work-order/src/views/tickets/ticketComponent/TicketDetailDialog.vue
File was deleted
applications/task-work-order/src/views/tool/code.vue
File was deleted
applications/task-work-order/src/views/tool/datasource.vue
File was deleted
applications/task-work-order/src/views/tool/model.vue
File was deleted
applications/task-work-order/src/views/wel/components/backlog.vue
File was deleted
applications/task-work-order/src/views/wel/components/calendarBox.vue
File was deleted
applications/task-work-order/src/views/wel/components/flightStatistics.vue
File was deleted
applications/task-work-order/src/views/wel/components/flyratio.vue
File was deleted
applications/task-work-order/src/views/wel/components/proportionStatic.vue
File was deleted
applications/task-work-order/src/views/wel/components/statistics.vue
File was deleted
applications/task-work-order/src/views/wel/components/taskOutcome.vue
File was deleted
applications/task-work-order/src/views/wel/dashboard.vue
File was deleted
applications/task-work-order/src/views/wel/index.vue
@@ -1,217 +1,13 @@
<template>
  <wel-container>
    <div class="workbench" v-if="display">
      <div class="workleft">
        <!-- 设备统计  -->
        <statistics></statistics>
        <!-- 综合统计分析 -->
        <div class="comprehensiveCon">
          <div class="comprehensive">
    <div>
            <div class="center">
              <div class="centerLeft">
                <!-- 工单统计 -->
                <proportionStatic></proportionStatic>
                <!-- 飞行统计 -->
                <flightStatistics></flightStatistics>
              </div>
              <div class="centerRight">
                <!-- 机巢工单数量排名(件) -->
                <flyratio></flyratio>
                <!-- 任务成果 -->
                <taskOutcome></taskOutcome>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="workright">
        <Bocklog></Bocklog>
        <CalenBox></CalenBox>
      </div>
    </div>
  </wel-container>
    </div>
</template>
<script setup>
import taskOutcome from './components/taskOutcome.vue'
import flightStatistics from './components/flightStatistics.vue'
import proportionStatic from './components/proportionStatic.vue'
import flyratio from './components/flyratio.vue'
import Bocklog from './components/backlog.vue'
import CalenBox from './components/calendarBox.vue'
import * as echarts from 'echarts'
import useEchartsResize from '@/hooks/useEchartsResize'
import { mapGetters } from 'vuex'
import { getJobEventByStatus, getJobEventTotal, getFly, getFlyTime } from '@/api/home/index'
import overviewImg1 from '@/assets/images/workbench/tc1.png'
import fy1 from '@/assets/images/workbench/fy1.png'
import statistics from './components/statistics.vue'
import { ElMessage } from 'element-plus'
let checked = ref('CURRENT_YEAR')
const display = ref(false)
let timeListStr = ['本周', '本月', '本年']
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR']
const params = ref({
  date_enum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
})
const dateSelect = ref('CURRENT_YEAR')
let timeClick = (item, index) => {
  checked.value = item
  params.value.date_enum = item
  dateSelect.value = item
}
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR'
  checked.value = 'CURRENT_YEAR'
  dateSelect.value = 'CURRENT_YEAR'
}
// 跳转
const jumpshebei = () => {
  ElMessage.warning('加急开发中...')
}
const eventTotal = ref(0)
const data = ref([])
onMounted(() => {
  getJobEventTotal().then(res => {
    eventTotal.value = res?.data?.data || 0
  })
  setTimeout(() => {
    display.value = true
  },100)
})
</script>
<style>
.el-font-size {
  font-size: 14px;
}
</style>
<style scoped lang="scss">
.workbench {
  height: 100%;
  flex: 1;
  // padding: 0px 20px 0 10px;
  display: flex;
  justify-content: space-between;
}
<style lang="scss" scoped>
.workleft {
  width: 68%;
  margin-right: 10px;
  height: 100%;
  .comprehensiveCon {
    // background: #ffffff !important;
    height: pxToVh(776);
    border-radius: 8px 8px 8px 8px;
    .comprehensive {
      // padding: 14px 14px 0 21px;
      .title {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .name {
          display: flex;
          align-items: center;
          font-weight: bold;
          font-size: 16px;
          color: #363636;
          font-family: 'Source Han Sans CN';
          span {
            margin-right: 4px;
          }
          img {
            cursor: pointer;
          }
        }
        .arrow {
          cursor: pointer;
        }
        .time-card {
          text-align: center;
          // height: 30px;
          height: pxToVh(30);
          background: #ffffff;
          border-radius: 4px 0px 0px 4px;
          border: 1px solid #e5e5e5;
          font-weight: 400;
          font-size: 14px;
          color: #7c8091;
          display: flex;
          width: 282px;
          margin-left: 15px;
          .card-item {
            width: 94px;
            height: 100%;
            line-height: 28px;
            cursor: pointer;
            font-family: 'Source Han Sans CN';
            font-weight: 400;
            font-size: 14px;
            color: #7c8091;
          }
          .card-item:first-child {
            border-right: 1px solid #e5e5e5;
          }
          .card-item:nth-child(2) {
            border-right: 1px solid #e5e5e5;
          }
          .card-item.active {
            color: #1441ff;
            border: 1px solid #1c5cff;
          }
        }
      }
      // 工、单
      .center {
        display: flex;
        .centerLeft {
          width: 50%;
        }
        .centerRight {
          width: 50%;
        }
      }
    }
  }
}
.workright {
  width: 0;
  flex: 1;
  height: 100%;
  // display: flex;
  // flex-direction: column;
}
</style>
</style>
applications/task-work-order/src/views/workspace/index.vue
File was deleted
uniapps/work-app/src/hooks/useGlobalWS.js
@@ -1,5 +1,4 @@
import { useUserStore } from "@/store/index.js";
import { ref, computed, watch } from "vue";
import useAppStore from "../store/modules/app/index.js";
import websocketService from "@/utils/websocket.js";
@@ -34,81 +33,24 @@
      // 转义参数以便在URL中传递
      const encodedParams = encodeURIComponent(JSON.stringify(callParams));
      // 跳转到mobile-web-view应用中的语音通话页面
      const voiceCallUrl = `http://localhost:9527/work-app/#/subPackages/voiceCallDetail/index?voiceparams=${encodedParams}`;
      console.log('🔗 准备导航到:', voiceCallUrl)
      try {
        // 优先使用uni-app的导航API(适用于uni-app环境)
        // 优先使用uni-app的导航API(适用于同应用内跳转)
        if (typeof uni !== 'undefined' && uni.navigateTo) {
          console.log('使用uni-app导航API跳转到语音通话页面');
          // 检查是否是跳转到应用内页面
          if (voiceCallUrl.includes('/subPackages/voiceCallDetail/')) {
            // 应用内页面跳转
            uni.navigateTo({
              url: voiceCallUrl.replace('http://localhost:9527/work-app/#', ''),
              success: () => {
                console.log('✅ 应用内导航成功!');
              },
              fail: (err) => {
                console.error('❌ 应用内导航失败:', err);
                // 如果应用内导航失败,尝试使用外部跳转
                fallbackToExternalNavigation();
              }
            });
          } else {
            // 外部页面跳转
            fallbackToExternalNavigation();
          }
        }
        // 否则尝试使用window.location跳转(适用于Web环境)
        else if (typeof window !== 'undefined' && window.location) {
          console.log('使用window.location跳转到语音通话页面');
          window.location.href = voiceCallUrl;
          uni.navigateTo({
            url: `/subPackages/voiceCallDetail/index?voiceparams=${encodedParams}`,
            success: () => {
              console.log('✅ 应用内导航成功!');
            },
            fail: (err) => {
              console.error('❌ 应用内导航失败:', err);
            }
          });
        } else {
          console.error('无法跳转到语音通话页面:当前环境不支持导航');
        }
      } catch (error) {
        console.error('跳转到语音通话页面失败:', error);
        // 尝试使用外部跳转作为最后的补救措施
        fallbackToExternalNavigation();
      }
      // 外部页面跳转的回调函数
      function fallbackToExternalNavigation() {
        console.log('尝试使用外部导航跳转到语音通话页面');
        try {
          // 使用uni-app的外部链接跳转API
          if (typeof uni !== 'undefined' && uni.navigateToWebview) {
            // 这是一个示例,具体API可能需要根据uni-app版本和配置调整
            uni.navigateToWebview({
              url: voiceCallUrl,
              success: () => {
                console.log('✅ 外部Webview导航成功!');
              },
              fail: (err) => {
                console.error('❌ 外部Webview导航失败:', err);
                // 如果还是失败,尝试使用浏览器打开
                if (typeof plus !== 'undefined' && plus.runtime) {
                  plus.runtime.openURL(voiceCallUrl);
                } else {
                  console.error('❌ 所有导航方式都失败了');
                }
              }
            });
          }
          // 或者使用plus.runtime.openURL(适用于App环境)
          else if (typeof plus !== 'undefined' && plus.runtime) {
            plus.runtime.openURL(voiceCallUrl);
            console.log('尝试使用plus.runtime.openURL打开外部链接');
          } else {
            console.error('无法跳转到外部页面:当前环境不支持外部导航');
          }
        } catch (fallbackError) {
          console.error('外部导航尝试失败:', fallbackError);
        }
      }
      return
    }
@@ -142,14 +84,24 @@
  // 初始化WebSocket连接
  function initWS() {
    try {
      // 使用默认用户ID初始化WebSocket连接
      websocketService.init(defaultUserId, WS_BASE);
      console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId);
      // 检查是否已经有活跃的WebSocket连接
      if (!websocketService.getConnected()) {
        // 使用默认用户ID初始化WebSocket连接
        websocketService.init(defaultUserId, WS_BASE);
        console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId);
      } else {
        console.log('🌐 WebSocket已存在活跃连接,无需重新初始化');
      }
      // 设置全局消息处理回调
      websocketService.setOnMessageCallback(messageHandler);
      console.log('✅ 全局WebSocket消息处理器已设置');
      // 只设置一次全局消息处理回调
      // 检查是否已经设置了回调
      if (typeof websocketService.getOnMessageCallback() !== 'function') {
        // 设置全局消息处理回调
        websocketService.setOnMessageCallback(messageHandler);
        console.log('✅ 全局WebSocket消息处理器已设置');
      } else {
        console.log('✅ WebSocket消息处理器已存在,无需重新设置');
      }
    } catch (error) {
      console.error('❌ 全局WebSocket初始化失败:', error);
    }
uniapps/work-app/src/pages/login/index.vue
@@ -135,59 +135,8 @@
      userInfo.key,
      userInfo.code
    );
    console.log('登录成功,用户信息:', res.data.data);
    userStore.setUserInfo(res.data.data);
    // 初始化WebSocket连接
    try {
      const wsUrl = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
      // 设置默认用户ID为3
      const defaultUserId = '3';
      // 使用默认用户ID初始化WebSocket连接
      websocketService.init(defaultUserId, wsUrl);
      console.log('登录成功,WebSocket已初始化,默认用户ID:', defaultUserId);
      // 设置来电消息回调
      websocketService.setOnMessageCallback((message) => {
        if (message.type === 'call') {
          console.log('收到来电,准备跳转到语音通话页面');
          // 构建来电参数
          const callParams = {
            peerUid: message.from,
            from: message.from,
            type: 'incoming'
          };
          // 转义参数以便在URL中传递
          const encodedParams = encodeURIComponent(JSON.stringify(callParams));
          try {
            // 优先使用uni-app的导航API(适用于同应用内跳转)
            if (typeof uni !== 'undefined' && uni.navigateTo) {
              console.log('使用uni-app导航API跳转到语音通话页面');
              uni.navigateTo({
                url: `/subPackages/voiceCallDetail/index?params=${encodedParams}`
              });
            }
            // 否则使用window.location跳转(适用于Web环境)
            else if (typeof window !== 'undefined' && window.location) {
              console.log('使用window.location跳转到语音通话页面');
              // 注意:实际部署时需要使用正确的URL
              const voiceCallUrl = `http://localhost:8080/appPages/voiceCallDetail/index.html?params=${encodedParams}`;
              window.location.href = voiceCallUrl;
            } else {
              console.error('无法跳转到语音通话页面:当前环境不支持导航');
            }
          } catch (error) {
            console.error('跳转到语音通话页面失败:', error);
          }
        }
      });
    } catch (error) {
      console.error('WebSocket初始化失败:', error);
    }
    console.log('登录成功,WebSocket连接将由全局钩子统一管理');
    uni.reLaunch({
      url: "/pages/work/index",
    });
uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -1,40 +1,58 @@
<!-- 工单详情 -->
<template>
  <div class="workDetailContainer">
    <!-- 自定义导航栏 -->
    <u-navbar
      title="工单详情"
      :is-back="true"
      back-text=""
      :back-icon-size="40"
      @left-click="onBackClick"
    >
      <template #right>
        <div class="share-btn" @click="onShareClick">分享</div>
      </template>
    </u-navbar>
    <div class="detailTop">
      <div class="image-container">
        <u-swiper
          class="detailImage"
          :list="getImageList"
          mode="round"
          indicator
          indicatorMode="dot"
          indicatorActiveColor="#4C85FF"
          indicatorInactiveColor="#fff"
          indicatorPosition="center"
          height="410rpx"
          @click="previewImage"
        ></u-swiper>
        <!--                <div class="detailTitle">-->
        <!--                    <div class="titleText">-->
        <!--                        <div class="itemName">{{workDetailData.event_name}}</div>-->
        <!--                        <div class="itemTime">{{formatDate(workDetailData.create_time)}}</div>-->
        <!--                    </div>-->
        <!--                </div>-->
      </div>
    </div>
    <div class="workDetailContainer">
        <!-- 自定义导航栏 -->
        <u-navbar title="工单详情" :is-back="true" back-text="" :back-icon-size="40" @left-click="onBackClick">
            <template #right>
                <div class="share-btn" @click="onShareClick">分享</div>
            </template>
        </u-navbar>
        <!-- 分享弹出层 -->
        <div v-if="showShareModal" class="share-modal-overlay" @click="onShareModalClose">
            <div class="share-modal-content" @click.stop>
                <div class="share-modal-header">
                    <div class="share-modal-title">分享到</div>
                    <div class="share-modal-close" @click="onShareModalClose">×</div>
                </div>
                <div class="share-modal-body">
                    <div class="share-item" @click="shareToWechat">
                        <div class="share-icon wechat"></div>
                        <div class="share-name">微信好友</div>
                    </div>
                    <div class="share-item" @click="shareToMoments">
                        <div class="share-icon moments"></div>
                        <div class="share-name">朋友圈</div>
                    </div>
                    <div class="share-item" @click="copyLink">
                        <div class="share-icon link"></div>
                        <div class="share-name">复制链接</div>
                    </div>
                </div>
            </div>
        </div>
        <div class="detailTop">
            <div class="image-container">
                <u-swiper
                    class="detailImage"
                    :list="getImageList"
                    mode="round"
                    indicator
                    indicatorMode="dot"
                    indicatorActiveColor="#4C85FF"
                    indicatorInactiveColor="#fff"
                    indicatorPosition="center"
                    height="410rpx"
                    @click="previewImage"
                ></u-swiper>
                <!--                <div class="detailTitle">-->
                <!--                    <div class="titleText">-->
                <!--                        <div class="itemName">{{workDetailData.event_name}}</div>-->
                <!--                        <div class="itemTime">{{formatDate(workDetailData.create_time)}}</div>-->
                <!--                    </div>-->
                <!--                </div>-->
            </div>
        </div>
    <!-- 工单内容 -->
    <div class="worderContainer">
@@ -178,88 +196,111 @@
  })
}
// 分享按钮点击事件
// 分享模态框状态
const showShareModal = ref(false)
// 分享按钮点击事件 - 显示分享模态框
const onShareClick = () => {
  // 检查当前平台是否支持分享功能
  const platform = uni.getSystemInfoSync().platform
    showShareModal.value = true
}
  // H5平台不支持uni.share API
  if (platform === 'web') {
    uni.showToast({
      title: 'H5平台暂不支持分享功能',
      icon: 'none'
    })
    return
  }
// 关闭分享模态框
const onShareModalClose = () => {
    showShareModal.value = false
}
  uni.showActionSheet({
    itemList: ['分享给微信好友', '分享到朋友圈'],
    success: res => {
      // 检查uni.share是否存在
      if (typeof uni.share !== 'function') {
        uni.showToast({
          title: '当前环境不支持分享功能',
          icon: 'none'
        })
        return
      }
// 微信好友分享
const shareToWechat = () => {
    shareToPlatform('weixin', 'WXSceneSession')
}
      try {
        switch (res.tapIndex) {
          case 0:
            // 分享给微信好友
            uni.share({
              provider: 'weixin',
              scene: 'WXSceneSession',
              type: 0,
              title: '工单详情',
              summary: workDetailData.value.event_name,
              href: 'https://example.com/workDetail?eventNum=' + workDetailData.value.event_num,
              imageUrl: workDetailData.value.photo_url,
              success: () => {
                uni.showToast({ title: '分享成功', icon: 'success' })
              },
              fail: (err) => {
                console.log('分享失败', err.errMsg)
                uni.showToast({
                  title: err.errMsg,
                  icon: 'none'
                })
              }
            })
            break
          case 1:
            // 分享到朋友圈
            uni.share({
              provider: 'weixin',
              scene: 'WXSceneTimeline',
              type: 0,
              title: workDetailData.value.event_name,
              summary: '查看工单详情',
              href: 'https://example.com/workDetail?eventNum=' + workDetailData.value.event_num,
              imageUrl: workDetailData.value.photo_url,
              success: () => {
                uni.showToast({ title: '分享成功', icon: 'success' })
              },
              fail: (err) => {
                console.log('分享失败', err)
                uni.showToast({
                  title: '分享失败,请检查配置',
                  icon: 'none'
                })
              }
            })
            break
        }
      } catch (error) {
        console.error('分享出错:', error)
        uni.showToast({
          title: '分享功能异常',
          icon: 'none'
        })
      }
    }
  })
// 朋友圈分享
const shareToMoments = () => {
    shareToPlatform('weixin', 'WXSceneTimeline')
}
// 复制链接
const copyLink = async () => {
    const shareUrl = 'https://example.com/workDetail?eventNum=' + workDetailData.value.event_num
    try {
        await uni.setClipboardData({
            data: shareUrl,
        })
        uni.showToast({
            title: '链接已复制',
            icon: 'success',
        })
    } catch (error) {
        console.error('复制失败:', error)
        uni.showToast({
            title: '复制失败,请重试',
            icon: 'none',
        })
    }
    onShareModalClose()
}
// 通用分享平台函数
const shareToPlatform = (provider, scene = '') => {
    // 检查当前平台是否支持分享功能
    const platform = uni.getSystemInfoSync().platform
    // H5平台不支持uni.share API
    if (platform === 'web') {
        uni.showToast({
            title: 'H5平台暂不支持分享功能',
            icon: 'none',
        })
        onShareModalClose()
        return
    }
    // 检查uni.share是否存在
    if (typeof uni.share !== 'function') {
        uni.showToast({
            title: '当前环境不支持分享功能',
            icon: 'none',
        })
        onShareModalClose()
        return
    }
    try {
        const shareConfig = {
            provider,
            type: 0,
            title: workDetailData.value.event_name || '工单详情',
            summary: '查看工单详情',
            href: 'https://example.com/workDetail?eventNum=' + workDetailData.value.event_num,
            imageUrl: workDetailData.value.photo_url,
            success: () => {
                uni.showToast({ title: '分享成功', icon: 'success' })
                onShareModalClose()
            },
            fail: err => {
                console.log('分享失败', err)
                uni.showToast({
                    title: '分享失败,请重试',
                    icon: 'none',
                })
                onShareModalClose()
            },
        }
        // 添加场景参数(如果有)
        if (scene) {
            shareConfig.scene = scene
        }
        uni.share(shareConfig)
    } catch (error) {
        console.error('分享出错:', error)
        uni.showToast({
            title: '分享功能异常',
            icon: 'none',
        })
        onShareModalClose()
    }
}
// 返回按钮点击事件
@@ -422,4 +463,103 @@
    }
  }
}
/* 分享弹出层样式 */
.share-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.6);
    display: flex;
    justify-content: flex-end;
    align-items: flex-end;
    z-index: 9999;
}
.share-modal-content {
    background-color: #fff;
    border-radius: 20rpx 20rpx 0 0;
    padding: 30rpx;
    width: 100%;
    max-height: 80vh;
    overflow-y: auto;
}
.share-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30rpx;
}
.share-modal-title {
    font-size: 36rpx;
    font-weight: bold;
    color: #222324;
    text-align: center;
    flex: 1;
}
.share-modal-close {
    font-size: 60rpx;
    color: #999;
    line-height: 40rpx;
}
.share-modal-body {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    padding-bottom: 30rpx;
}
.share-modal-footer {
    display: flex;
    justify-content: space-around;
    padding-top: 30rpx;
    border-top: 1px solid #f0f0f0;
}
.share-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 20rpx;
    width: 120rpx;
}
.share-icon {
    width: 90rpx;
    height: 90rpx;
    border-radius: 50%;
    margin-bottom: 15rpx;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
}
.share-icon.wechat {
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%2307C160" d="M18.66 6.86l-1.41-1.42a1 1 0 00-1.42 0l-12 12a1 1 0 000 1.42l1.42 1.42a1 1 0 001.42 0l12-12a1 1 0 000-1.42z"/><path fill="%2307C160" d="M8.5 14.5c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm0-6a2 2 0 100 4 2 2 0 000-4zm7 6c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm0-6a2 2 0 100 4 2 2 0 000-4z"/></svg>');
}
.share-icon.moments {
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%2307C160" d="M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H6V4h12v16z"/><path fill="%2307C160" d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"/><path fill="%2307C160" d="M10 13.5h4v3h-4z"/></svg>');
}
.share-icon.link {
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23666" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
}
.share-icon.poster {
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23FFA500" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/><path fill="%23FFA500" d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>');
}
.share-name {
    font-size: 24rpx;
    color: #666;
    margin-top: 10rpx;
}
</style>
uniapps/work-app/src/utils/websocket.js
@@ -160,8 +160,14 @@
  // 设置消息回调
  setOnMessageCallback(callback) {
    console.log('🔔 WebSocket消息回调已设置')
    this.onMessageCallback = callback
  }
  // 获取当前消息回调(用于检查是否已设置)
  getOnMessageCallback() {
    return this.onMessageCallback
  }
  // 设置连接打开回调
  setOnOpenCallback(callback) {