吉安感知网项目-前端
罗广辉
2026-01-15 04a859aa95b231c032c71d06518497a973c2f4e8
Merge remote-tracking branch 'origin/master'
63 files modified
1 files added
7 files deleted
4725 ■■■■■ changed files
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/top/index.vue 13 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/top/top-qna.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/store/modules/user.js 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common/cockpit.scss 537 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/aICrudTemplate/index.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/areaStatistics/index.vue 20 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue 166 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/defenseZone/index.vue 16 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue 265 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/index.vue 10 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue 80 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/index.vue 18 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue 257 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/index.vue 22 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceScrap/index.vue 24 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/index.vue 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord/index.vue 20 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCenter/components/dataCenterMap.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/FormDiaLog.vue 63 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/index.vue 204 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/detectionRange/DetectionRangeDialog.vue 327 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/detectionRange/index.vue 23 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/FormDiaLog.vue 126 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/index.vue 126 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/FormDiaLog.vue 115 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/index.vue 28 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/job/components/JobRelatedEvents.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/job/components/TaskIntermediateContent/AddTask.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/job/components/executeNestDetails.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/leftList.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/layerManagement/components/rightEdit.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/alarmRecords/index.vue 20 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/historyTracks/TrajectoryDiaLog.vue 110 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/historyTracks/index.vue 14 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/resource/components/spotDetails.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/tickets/ticket.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/tickets/ticketComponent/DispatchDialog.vue 2 ●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue 961 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/hooks/useGlobalWS.js 239 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/login/index.vue 53 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue 2 ●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/common/index.js 2 ●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/websocket.js 188 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/api/index.js 2 ●●● patch | view | raw | blame | history
uniapps/work-wx/src/api/user/index.js 8 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/equipmentManagement/index.vue 60 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/legalPolicy/index.vue 79 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/login/index.vue 4 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/register/index.vue 16 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/user/index.vue 89 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/default-header.png patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/default-header.svg 17 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/department.svg 10 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/droneLogo.svg 11 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/info.svg 21 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/password.svg 26 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/rightBtn.svg 9 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/userbg.svg 38 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/deviceRegistration/add.vue 106 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/flightApplication/index.vue 118 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/regulationsDetail/details.vue 2 ●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/userDetail/infos/index.vue 5 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/userDetail/password/index.vue 7 ●●●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development
@@ -2,7 +2,7 @@
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-13 16:28:33
 # @LastEditTime : 2026-01-15 10:25:10
 # @FilePath     : \applications\drone-command\env\.env.development
 # @Description  : 
 # Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -16,7 +16,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.168
VITE_APP_URL= http://192.168.1.204
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/page/index/top/index.vue
@@ -8,12 +8,13 @@
    <div class="top-bar__right">
      <div class="top-user">
        <div class="icon-box">
         <img class="gateway" @click="jumpMH" src="@/assets/images/mh.png" alt="进入门户" title="进入门户">
          <img class="gateway" @click="jumpMH" src="@/assets/images/mh.png" alt="进入门户" title="进入门户">
        </div>
        <el-dropdown>
          <span class="el-dropdown-link">
            <img class="top-bar__img" :src="userInfo.avatar" alt="" />
            <!-- <img v-if="userInfo.avatar" class="top-bar__img top-bar__user__img" :src="userInfo.avatar" alt="" /> -->
            <img class="top-bar__img" :src="defaultAva" alt="" />
          </span>
          <template #dropdown>
@@ -36,6 +37,8 @@
</template>
<script>
import defaultAva from '@/assets/images/defaultava.png'
import logo from '@/assets/images/topContainer/logo.png'
import { mapGetters } from 'vuex'
@@ -67,6 +70,7 @@
  data () {
    return {
      logoUrl: logo,
      defaultAva: defaultAva
    }
  },
  filters: {},
@@ -190,6 +194,11 @@
  height: 21px;
}
.top-bar__user__img {
  width: 24px;
  height: 24px;
}
.el-dropdown-link {
  cursor: pointer;
  color: #606266;
applications/drone-command/src/page/index/top/top-qna.vue
@@ -25,7 +25,7 @@
const systemupdateFilePath = ref(null);
const rumenPath = ref(null);
const fileData = ref([]);
const loading = ref(false);
const loading = ref(true);
const getlistDataAPI = async () => {
  loading.value = true;
  try {
applications/drone-command/src/store/modules/user.js
@@ -8,7 +8,6 @@
import { setStore, getStore } from '@/utils/store';
import { validatenull } from '@/utils/validate';
import { deepClone } from '@/utils/util';
import defaultAva from '@/assets/images/defaultava.png'
import {
  loginByUsername,
  loginBySocial,
@@ -336,9 +335,6 @@
      if (validatenull(userInfo.user_id) && validatenull(userInfo.account)) {
        state.userInfo = { user_name: 'unauth', role_name: 'unauth' };
      } else {
        if (validatenull(userInfo.avatar)) {
          userInfo.avatar = defaultAva;
        }
        state.userInfo = userInfo;
      }
      setStore({ name: 'userInfo', content: state.userInfo });
applications/drone-command/src/styles/common/cockpit.scss
@@ -53,9 +53,8 @@
.ztzf-data-cockpit-date-picker-popper {
    background: #161B2C !important;
    border: 1px solid !important;
    border: none !important;
    border-radius: 0px 0px 8px 8px;
    border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
    .el-date-picker__prev-btn,
    .el-date-picker__next-btn {
@@ -73,6 +72,13 @@
            .el-picker-panel__body {
                background: #161B2C !important;
                color: #fff !important;
                .el-date-range-picker__time-header {
                    .el-input__wrapper {
                        box-shadow: none;
                        background: #2E2E48 !important;
                    }
                }
                .el-date-picker__time-header {
                    border: none;
@@ -107,7 +113,7 @@
                            }
                            .el-time-panel__footer {
                                background: #012350 !important;
                                background: #161B2C !important;
                                border-top: 1px solid rgba(71, 157, 255, 0.3) !important;
                                .el-button {
@@ -173,7 +179,7 @@
                        td.start-date span,
                        td.start-date span,
                        td.end-date span {
                            background-color: #012350 !important;
                            background-color: #161B2C !important;
                        }
                        td.in-range div,
@@ -197,7 +203,7 @@
    }
    .el-picker-panel__sidebar {
        background: #012350;
        background: #161B2C;
        color: #fff;
        .el-picker-panel__shortcut {
@@ -207,7 +213,7 @@
    .el-picker-panel__body {
        .el-input.is-disabled .el-input__wrapper {
            background: #012350;
            background: #161B2C;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
@@ -218,11 +224,11 @@
        .el-button.is-disabled,
        .el-button.is-disabled:hover {
            background: #012350;
            background: #161B2C;
        }
        .el-input__wrapper {
            background: #012350;
            background: #161B2C;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
@@ -233,18 +239,18 @@
    }
    .el-picker-panel__footer {
        background: #012350;
        background: #161B2C;
        color: #fff;
        border-top: none;
        .el-button--small {
            box-shadow: 0 0 0 0.1rem#409eff;
            background-color: #012350;
            background-color: #161B2C;
            color: #fff;
            border: none;
            &:hover {
                background-color: #012350;
                background-color: #161B2C;
                color: #fff;
                border-color: #409eff;
            }
@@ -258,6 +264,7 @@
    }
    .el-select__wrapper {
        padding: 0 8px;
        background: #2E2E48;
        box-shadow: none !important;
        border: none;
@@ -283,11 +290,11 @@
    }
    .el-select .el-input__wrapper {
        background-color: #012350 !important;
        background-color: #161B2C !important;
    }
    .el-input__wrapper {
        --el-input-text-color: #012350 !important;
        --el-input-text-color: #161B2C !important;
    }
}
@@ -698,6 +705,11 @@
    .el-button {
        min-width: 88px;
    }
    .el-button.is-disabled, .el-button.is-disabled:hover {
        background: #1A2652;
        border: none;
    }
}
.ztzf-table-container {
@@ -713,82 +725,82 @@
        flex: 1;
        display: flex;
        flex-direction: column;
        .ztzf-data-cockpit-table {
            height: 0;
            flex: 1;
            background: transparent !important;
            .el-table--border .el-table__inner-wrapper:after,
            .el-table--border:after,
            .el-table--border:before,
            .el-table__inner-wrapper:before {
                height: 0;
            }
            tr {
                background: transparent !important;
            }
            th.el-table__cell {
                height: 40px;
                border-bottom: 1px solid #323241;
                background: transparent !important;
                .cell {
                    padding: 0;
                    padding-left: 16px;
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 500;
                    font-size: 14px;
                    color: #FFFFFF;
                    line-height: 22px;
                    font-style: normal;
                    text-transform: none;
                }
            }
            td.el-table__cell {
                padding: 9px 0;
                height: 56px;
                border-bottom: 1px solid #323241;
                background: transparent !important;
                .cell {
                    padding: 0;
                    padding-left: 16px;
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 14px;
                    color: #D2D2E7;
                    line-height: 18px;
                    font-style: normal;
                    text-transform: none;
                }
            }
            td.operation-btns .cell {
                display: flex;
                flex-wrap: nowrap;
                gap: 12px;
                /* 按钮间距(推荐) */
                white-space: nowrap;
                /* 双保险 */
                a {
                    min-width: 28px;
                    color: #6A89FF;
                    &:last-child {
                        margin-right: 0;
                    }
                }
            }
        }
    }
    .ztzf-table-content-bg {
        background: #1A1A2A;
    }
}
.ztzf-data-cockpit-table {
    height: 0;
    flex: 1;
    background: transparent !important;
    .el-table--border .el-table__inner-wrapper:after,
    .el-table--border:after,
    .el-table--border:before,
    .el-table__inner-wrapper:before {
        height: 0;
    }
    tr {
        background: transparent !important;
    }
    th.el-table__cell {
        height: 40px;
        border-bottom: 1px solid #323241;
        background: transparent !important;
        .cell {
            padding: 0;
            padding-left: 16px;
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: 500;
            font-size: 14px;
            color: #FFFFFF;
            line-height: 22px;
            font-style: normal;
            text-transform: none;
        }
    }
    td.el-table__cell {
        padding: 9px 0;
        height: 56px;
        border-bottom: 1px solid #323241;
        background: transparent !important;
        .cell {
            padding: 0;
            padding-left: 16px;
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: 400;
            font-size: 14px;
            color: #D2D2E7;
            line-height: 18px;
            font-style: normal;
            text-transform: none;
        }
    }
    td.operation-btns .cell {
        display: flex;
        flex-wrap: nowrap;
        gap: 12px;
        /* 按钮间距(推荐) */
        white-space: nowrap;
        /* 双保险 */
        a {
            min-width: 28px;
            color: #6A89FF;
            &:last-child {
                margin-right: 0;
            }
        }
    }
}
@@ -862,6 +874,7 @@
}
.ztzf-page-view-dialog {
    padding: 30px;
    display: flex;
    flex-direction: column;
    width: 770px;
@@ -947,7 +960,6 @@
        }
        .dialog-form {
            .el-form-item {
                display: flex;
                align-items: center;
@@ -968,7 +980,7 @@
                .el-form-item__content {
                    display: flex;
                    align-items: center;
                    .el-input {
                        width: 0;
                        flex: 1;
@@ -989,6 +1001,44 @@
                        background: #2E2E48 !important;
                        border: none;
                        box-shadow: none;
                    }
                    .ztzf-data-cockpit-date-picker {
                        background: #2E2E48;
                    }
                    .ztzf-data-cockpit-date-picker-popper {
                        background: #2E2E48 !important;
                        .el-picker-panel {
                            .el-picker-panel__body-wrapper {
                                .el-picker-panel__body {
                                    background: #2E2E48 !important;
                                    .el-date-picker__time-header {
                                        .el-date-picker__editor-wrap {
                                            .el-time-panel {
                                                background: #2E2E48 !important;
                                                // 将 time-picker-popper 的样式移到这里
                                                .el-time-panel__header {
                                                    background: #2E2E48 !important;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        .el-popper__arrow {
                            &::before {
                                background: #2E2E48 !important;
                                border: 1px solid #2E2E48 !important;
                                box-sizing: border-box;
                            }
                        }
                    }
                }
@@ -1069,3 +1119,316 @@
    border-color: #2B2B4C !important;
    color: #FFFFFF !important;
}
.ztzf-page-map-view-dialog {
    padding: 30px;
    display: flex;
    flex-direction: column;
    width: 80%;
    height: 812px;
    background: #1A1A2A;
    border-radius: 6px 6px 6px 6px;
    border: 1px solid #2E2E46;
    .el-dialog__header,
    .el-dialog__footer {
        margin: 0;
        padding: 0;
    }
    .el-dialog__footer {
        .el-button {
            margin-top: 30px;
        }
    }
    .el-dialog__body {
        display: flex;
        flex-direction: column;
        height: 0;
        flex: 1;
        .dialog-container {
            display: flex;
            height: 0;
            flex: 1;
            .left-container {
                width: 0;
                flex: 1;
                height: 100%;
                .leftMap {
                    width: 100%;
                    height: 100%;
                }
            }
            .right-container {
                display: flex;
                flex-direction: column;
                width: 344px;
                height: 100%;
                .header {
                    padding: 0 16px;
                    padding-bottom: 16px;
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 18px;
                    color: #FFFFFF;
                    text-align: left;
                    font-style: normal;
                    text-transform: none;
                }
                .dialog-container {
                    padding: 0 16px;
                    height: 0;
                    flex: 1;
                    display: flex;
                    flex-direction: column;
                    .detail-title {
                        margin-bottom: 30px;
                        font-family: Source Han Sans CN, Source Han Sans CN;
                        font-weight: 500;
                        font-size: 18px;
                        color: #FFFFFF;
                        text-align: left;
                        font-style: normal;
                        text-transform: none;
                    }
                    .el-row {
                        .el-col {
                            margin-bottom: 20px;
                            display: flex;
                            align-items: center;
                            .label {
                                width: 112px;
                                font-family: Source Han Sans CN, Source Han Sans CN;
                                font-weight: 500;
                                font-size: 14px;
                                color: #D4D5D7;
                                line-height: 22px;
                                text-align: right;
                                font-style: normal;
                                text-transform: none;
                            }
                            .val {
                                width: 0;
                                flex: 1;
                                margin-left: 12px;
                                font-family: Source Han Sans CN, Source Han Sans CN;
                                font-weight: 400;
                                font-size: 14px;
                                color: #9E9EBA;
                                text-align: left;
                                font-style: normal;
                                text-transform: none;
                            }
                        }
                    }
                    .ztzf-table-container {
                        height: 360px;
                        .ztzf-table-pagination {
                            justify-content: center;
                        }
                    }
                }
                .dialog-form {
                    padding-left: 16px;
                    height: 0;
                    flex: 1;
                    display: flex;
                    flex-direction: column;
                    .el-form-item {
                        display: flex;
                        align-items: center;
                        .el-form-item__label {
                            display: flex;
                            align-items: center;
                            font-family: Source Han Sans CN, Source Han Sans CN;
                            font-weight: 500;
                            font-size: 14px;
                            color: #D4D5D7;
                            line-height: 22px;
                            text-align: right;
                            font-style: normal;
                            text-transform: none;
                        }
                        .el-form-item__content {
                            display: flex;
                            align-items: center;
                            .el-input,
                            .el-input-number {
                                width: 0;
                                flex: 1;
                            }
                            .el-textarea {
                                .el-textarea__inner {
                                    color: #fff;
                                    background: #2E2E48 !important;
                                    border: none;
                                    box-shadow: none;
                                }
                            }
                            .el-button {
                                color: #fff;
                                background: #2E2E48 !important;
                                border: none;
                                box-shadow: none;
                            }
                            >div.val {
                                color: #fff;
                            }
                            .ztzf-data-cockpit-date-picker {
                                background: #2E2E48;
                            }
                            .ztzf-data-cockpit-date-picker-popper {
                                background: #2E2E48 !important;
                                .el-picker-panel {
                                    .el-picker-panel__body-wrapper {
                                        .el-picker-panel__body {
                                            background: #2E2E48 !important;
                                            .el-date-picker__time-header {
                                                .el-date-picker__editor-wrap {
                                                    .el-time-panel {
                                                        background: #2E2E48 !important;
                                                        // 将 time-picker-popper 的样式移到这里
                                                        .el-time-panel__header {
                                                            background: #2E2E48 !important;
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                                .el-popper__arrow {
                                    &::before {
                                        background: #2E2E48 !important;
                                        border: 1px solid #2E2E48 !important;
                                        box-sizing: border-box;
                                    }
                                }
                            }
                        }
                        .el-input {
                            .el-input__wrapper {
                                background: #2E2E48 !important;
                                .el-input__inner {
                                    color: #ffffff;
                                }
                            }
                        }
                    }
                    .search-table-container {
                        height: 0;
                        flex: 1;
                        display: flex;
                        flex-direction: column;
                        .search-box {
                            display: flex;
                            margin-bottom: 18px;
                            .label {
                                display: flex;
                                align-items: center;
                                justify-content: flex-end;
                                padding-right: 12px;
                                width: 96px;
                                font-family: Source Han Sans CN, Source Han Sans CN;
                                font-weight: 500;
                                font-size: 14px;
                                color: #D4D5D7;
                                font-style: normal;
                                text-transform: none;
                                box-sizing: border-box;
                            }
                            .el-input {
                                width: 0;
                                flex: 1;
                                .el-input__wrapper {
                                    background: #2E2E48 !important;
                                    .el-input__inner {
                                        color: #ffffff;
                                    }
                                }
                            }
                        }
                        .el-form-item {
                            height: 0;
                            flex: 1;
                            display: flex;
                            flex-direction: column;
                            .el-form-item__content {
                                width: 100%;
                                height: 0;
                                flex: 1;
                                display: flex;
                                flex-direction: column;
                                .el-table {
                                    height: 0;
                                    flex: 1;
                                }
                            }
                        }
                    }
                }
                .footer {
                    width: 100%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    .el-button {
                        margin-left: 16px;
                        width: 96px;
                        height: 36px;
                        &:first-child {
                            margin-left: 0;
                        }
                    }
                }
            }
        }
    }
}
applications/drone-command/src/views/aICrudTemplate/index.vue
@@ -110,7 +110,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的设备ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/areaManage/areaStatistics/index.vue
@@ -32,23 +32,23 @@
            <div class="ztzf-table-content ztzf-table-content-bg">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="areaName" show-overflow-tooltip width="150" label="区域名称" />
                    <el-table-column prop="areaType" show-overflow-tooltip width="130" label="区域类型">
                    <el-table-column prop="areaName" show-overflow-tooltip label="区域名称" />
                    <el-table-column prop="areaType" show-overflow-tooltip label="区域类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.areaType, dictObj.areaType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="sceneName" show-overflow-tooltip width="150" label="关联场景" />
                    <el-table-column prop="sceneType" show-overflow-tooltip width="130" label="场景类型">
                    <el-table-column prop="sceneName" show-overflow-tooltip label="关联场景" />
                    <el-table-column prop="sceneType" show-overflow-tooltip label="场景类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceCount" show-overflow-tooltip width="110" label="设备数量" />
                    <el-table-column prop="alarmCount" show-overflow-tooltip width="110" label="告警次数" />
                    <el-table-column prop="counterCount" show-overflow-tooltip width="120" label="侦测反制次数" />
                    <el-table-column prop="counterSuccessCount" show-overflow-tooltip width="130" label="反制成功次数" />
                    <el-table-column prop="controlEffect" show-overflow-tooltip width="110" label="管控效果" />
                    <el-table-column prop="deviceCount" show-overflow-tooltip label="设备数量" />
                    <el-table-column prop="alarmCount" show-overflow-tooltip label="告警次数" />
                    <el-table-column prop="counterCount" show-overflow-tooltip label="侦测反制次数" />
                    <el-table-column prop="counterSuccessCount" show-overflow-tooltip label="反制成功次数" />
                    <el-table-column prop="controlEffect" show-overflow-tooltip label="管控效果" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm(row)">查看</el-link>
@@ -87,7 +87,7 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue
@@ -1,64 +1,88 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="80%">
        <div class="bodyBox">
            <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            <div class="rightInfo">
                <div v-if="readonly">
                    <div>防区名称: {{ formData.zoneName }}</div>
                    <div>防区面积: {{ formatZoneArea(formData.zoneArea) }}</div>
                    <el-table :data="sceneList" row-key="id">
                        <el-table-column prop="sceneName" label="场景名称" />
                        <el-table-column prop="sceneType" label="场景类型">
                            <template v-slot="{ row }">
                                {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                            </template>
                        </el-table-column>
                    </el-table>
    <el-dialog class="ztzf-page-map-view-dialog" v-model="visible" :show-close="false" :close-on-click-modal="false">
        <div class="dialog-container">
            <div class="left-container">
                <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            </div>
            <div class="right-container">
                <div class="header">
                    {{ titleEnum[dialogMode] }}
                </div>
                <el-form v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" label-width="100px">
                <div class="dialog-container" v-if="readonly">
                    <div class="detail-title">防区详情</div>
                    <el-row>
                        <el-col :span="24">
                            <el-form-item label="防区名称" prop="zoneName">
                                <el-input v-model="formData.zoneName" maxlength="50" placeholder="请输入" clearable />
                            </el-form-item>
                            <div class="label">防区名称</div>
                            <div class="val">{{ formData.zoneName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="防区面积" prop="zoneArea">{{ formatZoneArea(formData.zoneArea) }}</el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <div>
                                <div>
                                    关联场景
                                    <el-input v-model="searchName" placeholder="请输入" clearable></el-input>
                                </div>
                                <el-form-item label="关联场景" prop="defenseSceneIds" label-width="0">
                                    <el-table
                                        ref="sceneTableRef"
                                        :data="sceneList.filter(item => item.sceneName.includes(searchName))"
                                        row-key="id"
                                        @selection-change="handleSceneSelectionChange"
                                    >
                                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                                        <el-table-column prop="sceneName" label="场景名称" />
                                        <el-table-column prop="sceneType" label="场景类型">
                                            <template v-slot="{ row }">
                                                {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                                            </template>
                                        </el-table-column>
                                    </el-table>
                                </el-form-item>
                            </div>
                            <div class="label">防区面积</div>
                            <div class="val">{{ formatZoneArea(formData.zoneArea) }}</div>
                        </el-col>
                    </el-row>
                    <div class="detail-title">关联场景</div>
                    <div class="ztzf-table-container">
                        <div class="ztzf-table-content">
                            <el-table class="ztzf-data-cockpit-table" :data="sceneList" row-key="id">
                                <el-table-column prop="sceneName" label="场景名称" />
                                <el-table-column prop="sceneType" label="场景类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                    </div>
                </div>
                <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
                    label-width="96px">
                    <el-form-item label="防区名称" prop="zoneName">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.zoneName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="防区面积" prop="zoneArea">
                        <div class="val">{{ formatZoneArea(formData.zoneArea) }}</div>
                    </el-form-item>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">
                                关联场景
                            </div>
                            <el-input class="ztzf-data-cockpit-search-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                        </div>
                        <el-form-item prop="defenseSceneIds" label-width="0">
                            <el-table class="ztzf-data-cockpit-table" ref="sceneTableRef"
                                :data="sceneList.filter(item => item.sceneName.includes(searchName))" row-key="id"
                                @selection-change="handleSceneSelectionChange">
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="sceneName" label="场景名称" />
                                <el-table-column prop="sceneType" label="场景类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-form-item>
                    </div>
                </el-form>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
                    <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
                        :disabled="submitting" @click="handleSubmit">
                        确定
                    </el-button>
                </div>
            </div>
        </div>
        <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>
@@ -106,12 +130,12 @@
}
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    if (!pointList.length) {
@@ -134,14 +158,14 @@
}
// 加载详情
async function loadDetail() {
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwDefenseZoneDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
}
// 获取场景列表
async function getSceneList() {
async function getSceneList () {
    const res = await fwDefenseSceneListApi({ filterSelected: 1, zoneId: formData.value.id })
    sceneList.value = res?.data?.data ?? []
    if (readonly.value) {
@@ -151,19 +175,19 @@
}
// 获取场景ID列表
function getSceneIdList() {
function getSceneIdList () {
    if (!formData.value.defenseSceneIds) return []
    return formData.value.defenseSceneIds.split(',')
}
// 关联场景变更
function handleSceneSelectionChange(rows) {
function handleSceneSelectionChange (rows) {
    selectedSceneRows.value = rows
    formData.value.defenseSceneIds = rows.map(item => item.id).join(',')
}
// 同步关联场景
function syncSceneSelection() {
function syncSceneSelection () {
    if (!sceneTableRef.value) return
    sceneTableRef.value.clearSelection()
    const ids = getSceneIdList()
@@ -178,13 +202,13 @@
}
// 格式化防区面积
function formatZoneArea(value) {
function formatZoneArea (value) {
    if (value == null || value === '') return ''
    return `${value}k㎡`
}
// 初始化地图实例
function initMap() {
function initMap () {
    const publicCesiumInstance = new PublicCesium({
        dom: 'mapContainer',
        flatMode: false,
@@ -210,14 +234,14 @@
}
// 新增模式绘制
function addPolygon() {
function addPolygon () {
    drawPolygonExample = new DrawPolygon(viewer)
    drawPolygonExample.initHandler(viewer)
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
// 编辑面
function editPolygon() {
function editPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    drawPolygonExample?.destroy()
@@ -232,7 +256,7 @@
}
// 查看面
function viewPolygon() {
function viewPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    const result = pointList.map(item => [item.longitude, item.latitude]).flat()
@@ -250,7 +274,7 @@
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedSceneRows.value = []
@@ -277,18 +301,4 @@
})
</script>
<style scoped lang="scss">
.bodyBox {
    display: flex;
    height: 600px;
    .leftMap {
        width: 70%;
        height: 100%;
    }
    .rightInfo {
        width: 30%;
        height: 100%;
        overflow: auto;
    }
}
</style>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/defenseZone/index.vue
@@ -14,7 +14,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" type="primary" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -22,11 +22,11 @@
                <el-table class="ztzf-data-cockpit-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="zoneName" show-overflow-tooltip width="150" label="防区名称" />
                    <el-table-column prop="zoneArea" show-overflow-tooltip width="130" label="防区面积(k㎡)" />
                    <el-table-column prop="sceneCount" show-overflow-tooltip width="120" label="场景数量" />
                    <el-table-column prop="areaCount" show-overflow-tooltip width="120" label="区域数量" />
                    <el-table-column prop="deviceCount" show-overflow-tooltip width="120" label="设备数量" />
                    <el-table-column prop="zoneName" show-overflow-tooltip label="防区名称" />
                    <el-table-column prop="zoneArea" show-overflow-tooltip label="防区面积(k㎡)" />
                    <el-table-column prop="sceneCount" show-overflow-tooltip label="场景数量" />
                    <el-table-column prop="areaCount" show-overflow-tooltip label="区域数量" />
                    <el-table-column prop="deviceCount" show-overflow-tooltip label="设备数量" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('view', row)">查看</el-link>
@@ -49,7 +49,7 @@
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, provide, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwDefenseZonePageApi, fwDefenseZoneRemoveApi } from './defenseZoneApi'
@@ -66,7 +66,7 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,10 +1,17 @@
<template>
    <el-dialog class="ztzf-page-view-dialog" v-model="visible" :title="titleEnum[dialogMode]"
        :close-on-click-modal="false" width="80%">
        <div class="bodyBox">
            <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            <div class="rightInfo">
                <div class="detail-container" v-if="readonly">
    <el-dialog class="ztzf-page-map-view-dialog" v-model="visible" :show-close="false"
        :close-on-click-modal="false">
        <div class="dialog-container">
            <div class="left-container">
                <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            </div>
            <div class="right-container">
                <div class="header">
                    {{ titleEnum[dialogMode] }}
                </div>
                <div class="dialog-container" v-if="readonly">
                    <div class="detail-title">区域详情</div>
                    <el-row>
                        <el-col :span="24">
@@ -60,126 +67,90 @@
                        </div>
                    </div>
                </div>
                <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
                    label-width="100px">
                    <el-row>
                        <el-col :span="24">
                            <el-form-item label="区域名称" prop="areaName">
                                <el-input class="ztzf-data-cockpit-search-input" v-model="formData.areaName"
                                    maxlength="50" placeholder="请输入" clearable />
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="区域位置" prop="longitude">
                                <div>{{ formatLocation(formData) }}</div>
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="区域面积" prop="areaSize">{{ formData.areaSize || '-' }}k㎡</el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="区域类型" prop="areaType">
                                <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                                    v-model="formData.areaType" placeholder="请选择" clearable>
                                    <el-option
                                        v-for="item in dictObj.areaType"
                                        :key="item.dictKey"
                                        :label="item.dictValue"
                                        :value="item.dictKey"
                                    />
                                </el-select>
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="响应机制" prop="responseMechanism">
                                <el-input class="ztzf-data-cockpit-search-input" v-model="formData.responseMechanism"
                                    maxlength="200" placeholder="请输入" clearable />
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="触发条件" prop="triggerCondition">
                                <el-input class="ztzf-data-cockpit-search-input" v-model="formData.triggerCondition"
                                    maxlength="200" placeholder="请输入" clearable />
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="管控级别" prop="controlLevel">
                                <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                                    v-model="formData.controlLevel" placeholder="请选择" clearable>
                                    <el-option
                                        v-for="item in dictObj.controlLevel"
                                        :key="item.dictKey"
                                        :label="item.dictValue"
                                        :value="item.dictKey"
                                    />
                                </el-select>
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="可飞行时段" prop="flyDateStart">
                                <el-date-picker
                                    class="ztzf-data-cockpit-date-picker"
                                    popper-class="ztzf-data-cockpit-date-picker-popper"
                                    v-model="flyDateRange"
                                    type="datetimerange"
                                    range-separator="至"
                                    start-placeholder="开始时间"
                                    end-placeholder="结束时间"
                                    value-format="YYYY-MM-DD HH:mm:ss"
                                    clearable
                                />
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="关联派出所" prop="policeStationId">
                                <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                                    v-model="formData.policeStationId" placeholder="请选择" clearable>
                                    <el-option
                                        v-for="item in policeStationOptions"
                                        :key="item.id"
                                        :label="item.stationName"
                                        :value="item.id"
                                    />
                                </el-select>
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <div>
                                <div>
                                    关联设备
                                    <el-input class="ztzf-data-cockpit-search-input" v-model="searchName" placeholder="请输入"
                                        clearable></el-input>
                                </div>
                                <el-form-item prop="deviceIds" label-width="0">
                                    <el-table
                                        class="ztzf-data-cockpit-table"
                                        ref="deviceTableRef"
                                        :data="deviceOptions.filter(item => item.deviceName.includes(searchName))"
                                        row-key="id"
                                        @selection-change="handleDeviceSelectionChange"
                                    >
                                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                                        <el-table-column prop="deviceName" label="设备名称" />
                                        <el-table-column prop="deviceType" label="类型">
                                            <template v-slot="{ row }">
                                                {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                                            </template>
                                        </el-table-column>
                                    </el-table>
                                </el-form-item>
                    label-width="96px">
                    <el-form-item label="区域名称" prop="areaName">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.areaName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="区域位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                    </el-form-item>
                    <el-form-item label="区域面积" prop="areaSize">
                        <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
                    </el-form-item>
                    <el-form-item label="区域类型" prop="areaType">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.areaType" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="响应机制" prop="responseMechanism">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.responseMechanism"
                            maxlength="200" placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="触发条件" prop="triggerCondition">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.triggerCondition"
                            maxlength="200" placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="管控级别" prop="controlLevel">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.controlLevel" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.controlLevel" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="可飞行时段" prop="flyDateStart">
                        <el-date-picker class="ztzf-data-cockpit-date-picker"
                            popper-class="ztzf-data-cockpit-date-picker-popper" v-model="flyDateRange"
                            type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
                            value-format="YYYY-MM-DD HH:mm:ss" clearable />
                    </el-form-item>
                    <el-form-item label="关联派出所" prop="policeStationId">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.policeStationId" placeholder="请选择" clearable>
                            <el-option v-for="item in policeStationOptions" :key="item.id" :label="item.stationName"
                                :value="item.id" />
                        </el-select>
                    </el-form-item>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">
                                关联设备
                            </div>
                        </el-col>
                    </el-row>
                            <el-input class="ztzf-data-cockpit-search-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                        </div>
                        <el-form-item prop="deviceIds" label-width="0">
                            <el-table class="ztzf-data-cockpit-table" ref="deviceTableRef"
                                :data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id"
                                @selection-change="handleDeviceSelectionChange">
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="deviceName" label="设备名称" />
                                <el-table-column prop="deviceType" label="类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-form-item>
                    </div>
                </el-form>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
                    <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
                        :disabled="submitting" @click="handleSubmit">
                        确定
                    </el-button>
                </div>
            </div>
        </div>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting"
                @click="handleSubmit">
                确定
            </el-button>
        </template>
    </el-dialog>
</template>
@@ -218,7 +189,7 @@
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const readonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const titleEnum = ref({ edit: '编辑场景', view: '查看场景', add: '新增场景' })
const flyDateRange = ref([]) // 飞行时间
const deviceTableRef = ref(null)
const searchName = ref('')
@@ -243,12 +214,12 @@
}
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -273,7 +244,7 @@
)
// 加载详情
async function loadDetail() {
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwAreaDivideDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
@@ -281,19 +252,19 @@
}
// 格式化区域位置
function formatLocation(row) {
function formatLocation (row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
// 格式化可飞行时间段
function formatFlyDate(row) {
function formatFlyDate (row) {
    if (!row?.flyDateStart && !row?.flyDateEnd) return ''
    return `${row.flyDateStart || '-'} ~ ${row.flyDateEnd || '-'}`
}
// 初始化地图实例
function initMap() {
function initMap () {
    const publicCesiumInstance = new PublicCesium({
        dom: 'mapContainer',
        flatMode: false,
@@ -321,14 +292,14 @@
}
// 新增模式绘制
function addPolygon() {
function addPolygon () {
    drawPolygonExample = new DrawPolygon(viewer)
    drawPolygonExample.initHandler(viewer)
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
// 编辑面
function editPolygon() {
function editPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    drawPolygonExample?.destroy()
@@ -343,7 +314,7 @@
}
// 查看面
function viewPolygon() {
function viewPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    const result = pointList.map(item => [item.longitude, item.latitude]).flat()
@@ -361,13 +332,13 @@
}
// 获取派出所列表
async function getPoliceStationList() {
async function getPoliceStationList () {
    const res = await fwPoliceStationListApi()
    policeStationOptions.value = res?.data?.data ?? []
}
// 获取设备列表
async function getDeviceList() {
async function getDeviceList () {
    if (deviceOptions.value.length) return
    const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id })
    deviceOptions.value = res?.data?.data ?? []
@@ -377,14 +348,14 @@
}
// 关联设备变更
function handleDeviceSelectionChange(rows) {
function handleDeviceSelectionChange (rows) {
    selectedDeviceRows.value = rows
    const ids = rows.map(item => item.id)
    formData.value.deviceIds = ids.join(',')
}
// 同步选择状态
function syncDeviceSelection() {
function syncDeviceSelection () {
    if (!deviceTableRef.value) return
    deviceTableRef.value.clearSelection()
    const rows = []
@@ -399,7 +370,7 @@
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedDeviceRows.value = []
@@ -429,24 +400,4 @@
})
</script>
<style scoped lang="scss">
.bodyBox {
    display: flex;
    height: 600px;
    .leftMap {
        width: 70%;
        height: 100%;
    }
    .rightInfo {
        width: 30%;
        height: 100%;
        overflow: auto;
        .ztzf-table-container,
        .ztzf-table-content {
            height: auto;
            flex: none;
        }
    }
}
</style>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/partition/index.vue
@@ -22,7 +22,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -49,8 +49,8 @@
                            {{ getDictLabel(row.controlLevel, dictObj.controlLevel) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="policeStationName" show-overflow-tooltip width="140" label="关联派出所" />
                    <el-table-column show-overflow-tooltip width="160" label="可飞行时间段">
                    <el-table-column prop="policeStationName" show-overflow-tooltip label="关联派出所" />
                    <el-table-column show-overflow-tooltip label="可飞行时间段">
                        <template v-slot="{ row }">
                            {{ formatFlyDate(row) }}
                        </template>
@@ -77,7 +77,7 @@
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwAreaDividePageApi, fwAreaDivideRemoveApi } from './partitionApi'
@@ -94,7 +94,7 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
@@ -45,7 +45,7 @@
                <el-col :span="24">
                    <el-form-item label="位置" prop="longitude">
                        <el-button @click="selectLocation" style="width: 100%">
                            {{ formData.longitude }}, {{ formData.longitude }}, {{ formData.address }}
                            {{ formData.longitude }}, {{ formData.latitude }}, {{ formData.address }}
                        </el-button>
                    </el-form-item>
                </el-col>
@@ -59,11 +59,17 @@
            </el-button>
        </template>
        <el-dialog v-model="visible1" title="选择位置" destroy-on-close @closed="positionClosed">
            <div class="ztzf-cesium" id="mapContainer"></div>
        <el-dialog class="ztzf-page-map-view-dialog" v-model="visible1" :show-close="false"
            :close-on-click-modal="false" destroy-on-close @closed="positionClosed">
            <div class="dialog-container">
                <div class="left-container">
                    <div class="leftMap ztzf-cesium" id="mapContainer"></div>
                </div>
            </div>
            <template #footer>
                <el-button @click="handleMapCancel">取消</el-button>
                <el-button type="primary" @click="handleMapConfirm">确定</el-button>
                <el-button color="#2B2B4C" @click="handleMapCancel">取消</el-button>
                <el-button color="#284FE3" type="primary" @click="handleMapConfirm">确定</el-button>
            </template>
        </el-dialog>
    </el-dialog>
@@ -106,12 +112,12 @@
}
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -126,13 +132,13 @@
}
// 加载详情
async function loadDetail() {
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwPoliceStationDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? {}
}
function LeftClickEvent(click) {
function LeftClickEvent (click) {
    const pos = click.position // Cartesian2 屏幕坐标
    // 屏幕坐标 -> 椭球面坐标(不考虑地形/模型)
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
@@ -174,13 +180,49 @@
    }
}
function positionClosed() {
function positionClosed () {
    redPointEntity = null
}
let viewer
let redPointEntity
async function selectLocation() {
function setMapPoint (longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.45),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                pixelOffset: new Cesium.Cartesian2(0, -20),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
function formatCoord (value) {
    if (value == null || value === '') return '-'
    return _.round(Number(value), 6)
}
async function selectLocation () {
    visible1.value = true
    await nextTick()
    const publicCesiumInstance = new PublicCesium({
@@ -192,14 +234,18 @@
    })
    publicCesiumInstance.addLeftClickEvent(null, LeftClickEvent)
    viewer = publicCesiumInstance.getViewer()
    if (formData.value.longitude != null && formData.value.latitude != null) {
        tempLocation.value = { longitude: formData.value.longitude, latitude: formData.value.latitude }
        setMapPoint(formData.value.longitude, formData.value.latitude)
    }
}
function handleMapCancel() {
function handleMapCancel () {
    tempLocation.value = { longitude: null, latitude: null }
    visible1.value = false
}
async function getLocationName() {
async function getLocationName () {
    const tk = import.meta.env.VITE_APP_TDT_TOKEN
    const { latitude, longitude } = tempLocation.value
    const http = axios.create({ withCredentials: false })
@@ -210,7 +256,7 @@
    formData.value.address = res.data.result.formatted_address
}
function handleMapConfirm() {
function handleMapConfirm () {
    if (tempLocation.value.longitude != null && tempLocation.value.latitude != null) {
        formData.value.longitude = _.round(tempLocation.value.longitude, 6)
        formData.value.latitude = _.round(tempLocation.value.latitude, 6)
@@ -219,7 +265,7 @@
    visible1.value = false
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    dialogMode.value !== 'add' && (await loadDetail())
@@ -228,8 +274,4 @@
defineExpose({ open })
</script>
<style scoped lang="scss">
#mapContainer {
    width: 100%;
    height: 500px;
}
</style>
applications/drone-command/src/views/areaManage/precinctInfo/index.vue
@@ -14,7 +14,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -22,12 +22,12 @@
                <el-table class="ztzf-data-cockpit-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="stationName" show-overflow-tooltip width="160" label="派出所名称" />
                    <el-table-column prop="address" show-overflow-tooltip width="200" label="位置" />
                    <el-table-column prop="longitude" show-overflow-tooltip width="120" label="经度" />
                    <el-table-column prop="latitude" show-overflow-tooltip width="120" label="纬度" />
                    <el-table-column prop="contactPerson" show-overflow-tooltip width="120" label="联系人" />
                    <el-table-column prop="contactPhone" show-overflow-tooltip width="140" label="联系方式" />
                    <el-table-column prop="stationName" show-overflow-tooltip label="派出所名称" />
                    <el-table-column prop="address" show-overflow-tooltip label="位置" />
                    <el-table-column prop="longitude" show-overflow-tooltip label="经度" />
                    <el-table-column prop="latitude" show-overflow-tooltip label="纬度" />
                    <el-table-column prop="contactPerson" show-overflow-tooltip label="联系人" />
                    <el-table-column prop="contactPhone" show-overflow-tooltip label="联系方式" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('edit', row)">编辑</el-link>
@@ -49,7 +49,7 @@
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwPoliceStationPageApi, fwPoliceStationRemoveApi } from './precinctInfoApi'
@@ -62,7 +62,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -1,116 +1,137 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="80%">
        <div class="bodyBox">
            <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            <div class="rightInfo">
                <div v-if="readonly">
                    <div>场景名称: {{ formData.sceneName }}</div>
                    <div>指挥点位置: {{ formatLocation(formData) }}</div>
                    <div>场景类型: {{ getDictLabel(formData.sceneType, dictObj.sceneType) }}</div>
                    <div>防控负责人: {{ formData.defenseLeader }}</div>
                    <div>防控负责人电话: {{ formData.leaderPhone }}</div>
                    <div>设备模式: {{ getDictLabel(formData.deviceMode, dictObj.deviceMode) }}</div>
                    <div>防控面积: {{ formatDefenseArea(formData.defenseArea) }}</div>
                    <el-table ref="areaTableRef" :data="areaList" row-key="id">
                        <el-table-column prop="areaName" label="区域名称" />
                        <el-table-column prop="areaType" label="区域类型">
                            <template v-slot="{ row }">
                                {{ getDictLabel(row.areaType, dictObj.areaType) }}
                            </template>
                        </el-table-column>
                    </el-table>
    <el-dialog class="ztzf-page-map-view-dialog" v-model="visible" :show-close="false" :close-on-click-modal="false">
        <div class="dialog-container">
            <div class="left-container">
                <div class="leftMap ztzf-cesium" id="mapContainer"></div>
            </div>
            <div class="right-container">
                <div class="header">
                    {{ titleEnum[dialogMode] }}
                </div>
                <el-form v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" label-width="100px">
                <div class="dialog-container" v-if="readonly">
                    <div class="detail-title">场景详情</div>
                    <el-row>
                        <el-col :span="24">
                            <el-form-item label="场景名称" prop="sceneName">
                                <el-input v-model="formData.sceneName" maxlength="50" placeholder="请输入" clearable />
                            </el-form-item>
                            <div class="label">场景名称</div>
                            <div class="val">{{ formData.sceneName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="指挥点位置" prop="longitude">
                                <div>{{ formatLocation(formData) }}</div>
                            </el-form-item>
                            <div class="label">指挥点位置</div>
                            <div class="val">{{ formatLocation(formData) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="场景类型" prop="sceneType">
                                <el-select v-model="formData.sceneType" placeholder="请选择" clearable>
                                    <el-option
                                        v-for="item in dictObj.sceneType"
                                        :key="item.dictKey"
                                        :label="item.dictValue"
                                        :value="item.dictKey"
                                    />
                                </el-select>
                            </el-form-item>
                            <div class="label">场景类型</div>
                            <div class="val">{{ getDictLabel(formData.sceneType, dictObj.sceneType) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="防控负责人" prop="defenseLeader">
                                <el-input v-model="formData.defenseLeader" maxlength="50" placeholder="请输入" clearable />
                            </el-form-item>
                            <div class="label">负责人</div>
                            <div class="val">{{ formData.defenseLeader }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="防控负责人电话" prop="leaderPhone">
                                <el-input v-model="formData.leaderPhone" maxlength="50" placeholder="请输入" clearable />
                            </el-form-item>
                            <div class="label">负责人电话</div>
                            <div class="val">{{ formData.leaderPhone }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="设备模式" prop="deviceMode">
                                <el-select v-model="formData.deviceMode" placeholder="请选择" clearable>
                                    <el-option
                                        v-for="item in dictObj.deviceMode"
                                        :key="item.dictKey"
                                        :label="item.dictValue"
                                        :value="item.dictKey"
                                    />
                                </el-select>
                            </el-form-item>
                            <div class="label">设备模式</div>
                            <div class="val">{{ getDictLabel(formData.deviceMode, dictObj.deviceMode) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <el-form-item label="防控面积" prop="defenseArea">
                                <div>{{ formatDefenseArea(formData.defenseArea) }}</div>
                            </el-form-item>
                        </el-col>
                        <el-col :span="24">
                            <div>
                                <div>
                                    关联区域
                                    <el-input v-model="searchName" placeholder="请输入" clearable></el-input>
                                </div>
                                <el-form-item prop="areaDivideIds" label-width="0">
                                    <el-table
                                        ref="areaTableRef"
                                        :data="areaList.filter(item => item.areaName.includes(searchName))"
                                        row-key="id"
                                        @selection-change="handleAreaSelectionChange"
                                    >
                                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                                        <el-table-column prop="areaName" label="区域名称" />
                                        <el-table-column prop="areaType" label="区域类型">
                                            <template v-slot="{ row }">
                                                {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                            </template>
                                        </el-table-column>
                                    </el-table>
                                </el-form-item>
                            </div>
                            <div class="label">防控面积</div>
                            <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
                        </el-col>
                    </el-row>
                    <div class="detail-title">关联区域</div>
                    <div class="ztzf-table-container">
                        <div class="ztzf-table-content">
                            <el-table class="ztzf-data-cockpit-table" ref="areaTableRef" :data="areaList" row-key="id">
                                <el-table-column prop="areaName" label="区域名称" />
                                <el-table-column prop="areaType" label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                    </div>
                </div>
                <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
                    label-width="96px">
                    <el-form-item label="场景名称" prop="sceneName">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.sceneName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="指挥点位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                    </el-form-item>
                    <el-form-item label="场景类型" prop="sceneType">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.sceneType" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.sceneType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="负责人" prop="defenseLeader">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.defenseLeader" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="负责人电话" prop="leaderPhone">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.leaderPhone" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="设备模式" prop="deviceMode">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.deviceMode" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.deviceMode" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="防控面积" prop="defenseArea">
                        <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
                    </el-form-item>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">
                                关联区域
                            </div>
                            <el-input class="ztzf-data-cockpit-search-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                        </div>
                        <el-form-item prop="areaDivideIds" label-width="0">
                            <el-table class="ztzf-data-cockpit-table" ref="areaTableRef"
                                :data="areaList.filter(item => item.areaName.includes(searchName))" row-key="id"
                                @selection-change="handleAreaSelectionChange">
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="areaName" label="区域名称" />
                                <el-table-column prop="areaType" label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-form-item>
                    </div>
                </el-form>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
                    <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
                        :disabled="submitting" @click="handleSubmit">
                        确定
                    </el-button>
                </div>
            </div>
        </div>
        <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, inject, nextTick, onMounted, ref } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDefenseSceneDetailApi, fwDefenseSceneSubmitApi } from './sceneConfigApi'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
@@ -122,8 +143,8 @@
const initForm = () => ({
    sceneName: '', // 场景名称
    sceneType: '', // 场景类型
    defenseLeader: '', // 防控负责人
    leaderPhone: '', // 防控负责人电话
    defenseLeader: '', // 负责人
    leaderPhone: '', // 负责人电话
    deviceMode: '', // 设备模式
    areaDivideIds: '', // 关联区域ID
    areaCount: 0, // 区域数量
@@ -159,12 +180,12 @@
}
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -183,21 +204,21 @@
}
// 加载详情
async function loadDetail() {
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwDefenseSceneDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
}
// 获取区域列表
async function getAreaList() {
async function getAreaList () {
    if (areaList.value.length) return
    const res = await fwAreaDivideListApi({ filterSelected: 1, sceneId: formData.value.id })
    areaList.value = res?.data?.data ?? []
}
// 关联区域变更
function handleAreaSelectionChange(rows) {
function handleAreaSelectionChange (rows) {
    selectedAreaRows.value = rows
    formData.value.areaDivideIds = rows.map(item => item.id)
    formData.value.areaCount = rows.length
@@ -205,7 +226,7 @@
}
// 渲染面
function renderingSurface() {
function renderingSurface () {
    geometricSource && geometricSource.entities.removeAll()
    selectedAreaRows.value.forEach(item => {
        if (item.geom) {
@@ -227,13 +248,13 @@
watch(() => selectedAreaRows.value, renderingSurface)
function calcDefenseArea(rows) {
function calcDefenseArea (rows) {
    const total = rows.reduce((sum, item) => sum + (Number(item.areaSize) || 0), 0)
    return _.round(total, 2)
}
// 同步关联区域
function syncAreaSelection() {
function syncAreaSelection () {
    if (!areaTableRef.value) return
    areaTableRef.value.clearSelection()
    const rows = []
@@ -250,20 +271,20 @@
    formData.value.defenseArea = calcDefenseArea(rows)
}
function formatDefenseArea(value) {
function formatDefenseArea (value) {
    if (value == null || value === '') return ''
    return `${value}k㎡`
}
// 格式化指挥点位置
function formatLocation(row) {
function formatLocation (row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
let geometricSource
// 初始化地图实例
function initMap() {
function initMap () {
    const publicCesiumInstance = new PublicCesium({
        dom: 'mapContainer',
        flatMode: false,
@@ -279,7 +300,7 @@
    viewer.dataSources.add(geometricSource)
}
function LeftClickEvent(click) {
function LeftClickEvent (click) {
    const pos = click.position
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    if (!cartesian3) return
@@ -291,7 +312,7 @@
    setMapPoint(formData.value.longitude, formData.value.latitude)
}
function setMapPoint(longitude, latitude) {
function setMapPoint (longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
@@ -303,17 +324,17 @@
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE, // 文字颜色:白色
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.45), //背景颜色
                backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
                pixelOffset: new Cesium.Cartesian2(0, -20), // 可选:微调位置
                showBackground: true, // 确保背景显示(某些版本需要)
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.45),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                pixelOffset: new Cesium.Cartesian2(0, -20),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 负值更靠近相机(显示在前)
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
            },
        })
    } else {
@@ -323,7 +344,7 @@
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedAreaRows.value = []
@@ -348,18 +369,4 @@
})
</script>
<style scoped lang="scss">
.bodyBox {
    display: flex;
    height: 600px;
    .leftMap {
        width: 70%;
        height: 100%;
    }
    .rightInfo {
        width: 30%;
        height: 100%;
        overflow: auto;
    }
}
</style>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/sceneConfig/index.vue
@@ -55,7 +55,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -64,22 +64,22 @@
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="sceneName" show-overflow-tooltip width="130" label="场景名称" />
                    <el-table-column prop="sceneType" show-overflow-tooltip width="120" label="场景类型">
                    <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
                    <el-table-column prop="sceneType" show-overflow-tooltip label="场景类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.sceneType, dictObj.sceneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column show-overflow-tooltip width="150" label="指挥点位置">
                    <el-table-column show-overflow-tooltip label="指挥点位置">
                        <template v-slot="{ row }">
                            {{ formatLocation(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="defenseLeader" show-overflow-tooltip width="120" label="防控负责人" />
                    <el-table-column prop="leaderPhone" show-overflow-tooltip width="140" label="防控负责人电话" />
                    <el-table-column prop="areaCount" show-overflow-tooltip width="110" label="区域数量" />
                    <el-table-column prop="counterDeviceCount" show-overflow-tooltip width="120" label="反制设备数量" />
                    <el-table-column prop="detectDeviceCount" show-overflow-tooltip width="120" label="探测设备数量" />
                    <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
                    <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
                    <el-table-column prop="areaCount" show-overflow-tooltip label="区域数量" />
                    <el-table-column prop="counterDeviceCount" show-overflow-tooltip label="反制设备数量" />
                    <el-table-column prop="detectDeviceCount" show-overflow-tooltip label="探测设备数量" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('view', row)">查看</el-link>
@@ -107,7 +107,7 @@
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, provide, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwDefenseScenePageApi, fwDefenseSceneRemoveApi } from './sceneConfigApi'
@@ -125,7 +125,7 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/basicManage/deviceScrap/index.vue
@@ -31,21 +31,21 @@
                <el-table class="ztzf-data-cockpit-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="deviceName" show-overflow-tooltip width="130" label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip width="130" label="设备类型">
                    <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceModel" show-overflow-tooltip width="98" label="型号" />
                    <el-table-column prop="manufacturer" show-overflow-tooltip width="130" label="生产厂商" />
                    <el-table-column prop="createTime" show-overflow-tooltip width="96" label="入库时间" />
                    <el-table-column prop="source" show-overflow-tooltip width="112" label="来源" />
                    <el-table-column prop="charger" show-overflow-tooltip width="100" label="负责人" />
                    <el-table-column prop="scrapTime" show-overflow-tooltip width="96" label="报废时间" />
                    <el-table-column prop="scrapReason" show-overflow-tooltip width="136" label="报废原因" />
                    <el-table-column prop="disposeWay" show-overflow-tooltip width="116" label="处置方式" />
                    <el-table-column prop="maintainReminder" show-overflow-tooltip width="116" label="维护提醒" />
                    <el-table-column prop="deviceModel" show-overflow-tooltip label="型号" />
                    <el-table-column prop="manufacturer" show-overflow-tooltip label="生产厂商" />
                    <el-table-column prop="createTime" show-overflow-tooltip label="入库时间" />
                    <el-table-column prop="source" show-overflow-tooltip label="来源" />
                    <el-table-column prop="charger" show-overflow-tooltip label="负责人" />
                    <el-table-column prop="scrapTime" show-overflow-tooltip label="报废时间" />
                    <el-table-column prop="scrapReason" show-overflow-tooltip label="报废原因" />
                    <el-table-column prop="disposeWay" show-overflow-tooltip label="处置方式" />
                    <el-table-column prop="maintainReminder" show-overflow-tooltip label="维护提醒" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="handleView(row)">查看</el-link>
@@ -84,7 +84,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/basicManage/deviceStock/FormDiaLog.vue
@@ -272,7 +272,7 @@
    formData.value = initForm()
}
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
async function getList () {
    loading.value = true
applications/drone-command/src/views/basicManage/deviceStock/index.vue
@@ -70,7 +70,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="handleAdd">新增</el-button>
            <el-button :icon="Download" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
            <el-button :icon="Download" color="#1A2652" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
            <!--            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>-->
        </div>
@@ -155,7 +155,7 @@
    size: 10, // 每页大小
})
const searchParams = ref(initSearchParams()) // 查询参数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const total = ref(0) // 总数
const selectedIds = ref([]) // 勾选的设备ID列表
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue
@@ -198,7 +198,7 @@
})
const searchParams = ref(initSearchParams())
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
async function getList() {
    loading.value = true
applications/drone-command/src/views/basicManage/maintainRecord/index.vue
@@ -28,7 +28,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="handleAdd">维护计划</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -36,17 +36,17 @@
                <el-table class="ztzf-data-cockpit-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="deviceName" show-overflow-tooltip width="130" label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip width="130" label="设备类型">
                    <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceModel" show-overflow-tooltip width="98" label="型号" />
                    <el-table-column prop="manufacturer" show-overflow-tooltip width="130" label="生产厂商" />
                    <el-table-column prop="charger" show-overflow-tooltip width="100" label="负责人" />
                    <el-table-column prop="maintainReminder" show-overflow-tooltip width="116" label="维护提醒" />
                    <el-table-column prop="planCycleType" show-overflow-tooltip width="160" label="维护计划">
                    <el-table-column prop="deviceModel" show-overflow-tooltip label="型号" />
                    <el-table-column prop="manufacturer" show-overflow-tooltip label="生产厂商" />
                    <el-table-column prop="charger" show-overflow-tooltip label="负责人" />
                    <el-table-column prop="maintainReminder" show-overflow-tooltip label="维护提醒" />
                    <el-table-column prop="planCycleType" show-overflow-tooltip label="维护计划">
                        <template v-slot="{ row }">
                            {{ getPlanCycleLabel(row) }}
                        </template>
@@ -73,7 +73,7 @@
    </basic-container>
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDictionaryByCode } from '@/api/system/dictbiz'
@@ -95,7 +95,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/dataCenter/components/dataCenterMap.vue
@@ -233,7 +233,7 @@
};
// 地图接口
const loading = ref(false);
const loading = ref(true);
const getMapInfoAPIFun = async ids => {
  try {
    const res = await getMapInfoAPI(ids);
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue
@@ -108,7 +108,7 @@
})
const searchParams = ref(initSearchParams())
const queryParamsRef = ref(null)
const loading = ref(false)
const loading = ref(true)
const list = ref([])
const total = ref(0)
const areaTree = ref([
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue
@@ -33,7 +33,7 @@
import EmptyState from './EmptyState.vue'
const equipmentWarningData = ref([])
const loading = ref(false)
const loading = ref(true)
const minLoadingMs = 400
const statusMap = {
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue
@@ -44,7 +44,7 @@
import EmptyState from './EmptyState.vue'
const historyWarningData = ref([])
const loading = ref(false)
const loading = ref(true)
const minLoadingMs = 400
// 查询条件:keyword + dateRange(YYYY-MM-DD数组)
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue
@@ -36,7 +36,7 @@
import { ElMessage } from 'element-plus'
const realWarningData = ref([])
const loading = ref(false)
const loading = ref(true)
const minLoadingMs = 400
const actionTextMap = {
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -94,7 +94,7 @@
const historyVisible = ref(false)
const historyDevice = ref(null)
const loading = ref(false)
const loading = ref(true)
const minLoadingMs = 400
const onHistory = (item) => {
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/FormDiaLog.vue
@@ -1,53 +1,73 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false">
        <div v-if="readonly">
<template>
    <el-dialog
        class="ztzf-page-view-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        :close-on-click-modal="false"
        width="520px"
    >
        <div class="detail-container">
            <el-row>
                <el-col :span="12">
                    <div>无人机名称: {{ formData.droneName }}</div>
                    <div class="label">无人机名称</div>
                    <div class="val">{{ formData.droneName || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>无人机类型: {{ formData.droneType }}</div>
                    <div class="label">无人机类型</div>
                    <div class="val">{{ formData.droneType || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>无人机设备编码: {{ formData.droneDeviceCode }}</div>
                    <div class="label">无人机设备编码</div>
                    <div class="val">{{ formData.droneDeviceCode || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>发现时间: {{ formData.findTime }}</div>
                    <div class="label">发现时间</div>
                    <div class="val">{{ formData.findTime || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制效果: {{ getDictLabel(formData.counterEffect, dictObj.counterEffect) }}</div>
                    <div class="label">反制效果</div>
                    <div class="val">{{ getDictLabel(formData.counterEffect, dictObj.counterEffect) || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制方式: {{ getCounterWayLabel(formData.counterWay) }}</div>
                    <div class="label">反制方式</div>
                    <div class="val">{{ getCounterWayLabel(formData.counterWay) || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制设备名称: {{ formData.deviceName }}</div>
                    <div class="label">反制设备名称</div>
                    <div class="val">{{ formData.deviceName || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制设备型号: {{ formData.deviceModel }}</div>
                    <div class="label">反制设备型号</div>
                    <div class="val">{{ formData.deviceModel || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制设备类型: {{ getDeviceTypeLabel(formData.deviceType) }}</div>
                    <div class="label">反制设备类型</div>
                    <div class="val">{{ getDeviceTypeLabel(formData.deviceType) || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>反制设备编码: {{ formData.deviceSn }}</div>
                    <div class="label">反制设备编码</div>
                    <div class="val">{{ formData.deviceSn || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>区域编码: {{ formData.areaCode }}</div>
                    <div class="label">区域编码</div>
                    <div class="val">{{ formData.areaCode || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>部署位置: {{ getDeployPosition() }}</div>
                    <div class="label">部署位置</div>
                    <div class="val">{{ getDeployPosition() || '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>覆盖范围: {{ formData.coverRadiusM ? `${formData.coverRadiusM}米` : '' }}</div>
                    <div class="label">覆盖范围</div>
                    <div class="val">{{ formData.coverRadiusM ? `${formData.coverRadiusM}米` : '-' }}</div>
                </el-col>
                <el-col :span="12">
                    <div>工作模式: {{ getWorkModeLabel(formData.workMode) }}</div>
                    <div class="label">工作模式</div>
                    <div class="val">{{ getWorkModeLabel(formData.workMode) || '-' }}</div>
                </el-col>
            </el-row>
        </div>
        <template #footer>
            <el-button @click="handleCancel">关闭</el-button>
            <el-button color="#2B2B4C" @click="handleCancel">关闭</el-button>
        </template>
    </el-dialog>
</template>
@@ -78,7 +98,6 @@
// 注入字典数据
const dictObj = inject('dictObj')
// 注入反制效果选项
const formData = ref(initForm()) // 表单数据
const visible = defineModel() // 弹框显隐
@@ -94,7 +113,7 @@
// 获取反制方式标签
function getCounterWayLabel(value) {
    const map = { '1': '信号干扰', '2': '诱导驱离' }
    return map[value] || value || '无'
    return map[value] || value || '暂无'
}
// 获取设备类型标签
@@ -128,7 +147,9 @@
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'view'
    formData.value = row ? { ...row } : initForm()
    dialogMode.value === 'view' && (await loadDetail())
    if (readonly.value) {
        await loadDetail()
    }
}
defineExpose({ open })
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/index.vue
@@ -1,93 +1,114 @@
<template>
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams">
            <el-row :gutter="16">
                <el-col :span="4">
                    <el-form-item label="设备名称" prop="deviceName">
                        <el-input v-model="searchParams.deviceName" placeholder="请输入" clearable @clear="handleSearch" />
                    </el-form-item>
                </el-col>
                <el-col :span="4">
                    <el-form-item label="设备类型" prop="deviceType">
                        <el-select v-model="searchParams.deviceType" placeholder="请选择" clearable @change="handleSearch">
                            <el-option
                                v-for="item in dictObj.deviceType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="4">
                    <el-form-item label="反制效果" prop="counterEffect">
                        <el-select v-model="searchParams.counterEffect" placeholder="请选择" clearable @change="handleSearch">
                            <el-option
                                v-for="item in dictObj.counterEffect"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </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 ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search">
            <el-form-item label="设备名称" prop="deviceName">
                <el-input
                    class="ztzf-data-cockpit-search-input"
                    v-model="searchParams.deviceName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
                <el-select
                    class="ztzf-data-cockpit-select"
                    popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.deviceType"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.deviceType"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item label="反制效果" prop="counterEffect">
                <el-select
                    class="ztzf-data-cockpit-select"
                    popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.counterEffect"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.counterEffect"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item class="history-search-actions">
                <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button>
            </el-form-item>
        </el-form>
        <el-table v-loading="loading" :data="list">
            <el-table-column type="index" width="60" label="序号" />
            <el-table-column prop="droneName" label="无人机名称" />
            <el-table-column prop="droneType" label="无人机类型" />
            <el-table-column prop="droneDeviceCode" label="无人机设备编码" />
            <el-table-column prop="findTime" label="发现时间" />
            <el-table-column prop="counterEffect" label="反制效果">
                <template v-slot="{ row }">
                    {{ getDictLabel(row.counterEffect, dictObj.counterEffect) }}
                </template>
            </el-table-column>
            <el-table-column prop="deviceName" label="反制设备名称" />
            <el-table-column prop="deviceModel" label="反制设备型号" />
            <el-table-column prop="deviceType" label="反制设备类型">
                <template v-slot="{ row }">
                    {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                </template>
            </el-table-column>
            <el-table-column prop="deviceSn" label="反制设备编码" />
            <el-table-column prop="areaCode" label="场景" />
            <el-table-column prop="areaCode" label="区域" />
            <el-table-column label="部署位置">
                <template v-slot="{ row }">
                    {{ row.deployLongitude && row.deployLatitude ? `${row.deployLongitude}, ${row.deployLatitude}` : '' }}
                </template>
            </el-table-column>
            <el-table-column prop="coverRadiusM" label="覆盖范围">
                <template v-slot="{ row }">
                    {{ row.coverRadiusM ? `${row.coverRadiusM}米` : '' }}
                </template>
            </el-table-column>
            <el-table-column prop="workMode" label="工作模式">
                <template v-slot="{ row }">
                    {{ getWorkModeLabel(row.workMode) }}
                </template>
            </el-table-column>
            <el-table-column label="操作" width="100">
                <template v-slot="{ row }">
                    <el-link @click="openForm('view', row)" type="primary">查看</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 class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
            <div class="ztzf-table-content ztzf-table-content-bg">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" width="64" show-overflow-tooltip label="序号" />
                    <el-table-column prop="droneName" show-overflow-tooltip width="130" label="无人机名称" />
                    <el-table-column prop="droneType" show-overflow-tooltip width="130" label="无人机类型" />
                    <el-table-column prop="droneDeviceCode" show-overflow-tooltip width="140" label="无人机设备编码" />
                    <el-table-column prop="findTime" show-overflow-tooltip width="140" label="发现时间" />
                    <el-table-column prop="counterEffect" show-overflow-tooltip width="120" label="反制效果">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.counterEffect, dictObj.counterEffect) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceName" show-overflow-tooltip width="130" label="反制设备名称" />
                    <el-table-column prop="deviceModel" show-overflow-tooltip width="120" label="反制设备型号" />
                    <el-table-column prop="deviceType" show-overflow-tooltip width="120" label="反制设备类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceSn" show-overflow-tooltip width="140" label="反制设备编码" />
                    <el-table-column prop="areaCode" show-overflow-tooltip width="120" label="场景" />
                    <el-table-column prop="areaCode" show-overflow-tooltip width="120" label="区域" />
                    <el-table-column show-overflow-tooltip width="160" label="部署位置">
                        <template v-slot="{ row }">
                            {{ row.deployLongitude && row.deployLatitude ? `${row.deployLongitude}, ${row.deployLatitude}` : '' }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="coverRadiusM" show-overflow-tooltip width="120" label="覆盖范围">
                        <template v-slot="{ row }">
                            {{ row.coverRadiusM ? `${row.coverRadiusM}米` : '' }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="workMode" show-overflow-tooltip width="120" label="工作模式">
                        <template v-slot="{ row }">
                            {{ getWorkModeLabel(row.workMode) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" class-name="operation-btns" width="100">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('view', row)" type="primary">查看</el-link>
                        </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>
        <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" />
@@ -95,6 +116,7 @@
</template>
<script setup>
import { RefreshRight, Search } from '@element-plus/icons-vue'
import { onMounted, ref, nextTick, provide } from 'vue'
import { fwEffectEvalPageApi } from './countermeasureEvaluationApi'
import FormDiaLog from './FormDiaLog.vue'
@@ -112,17 +134,17 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
const dialogVisible = ref(null) // 弹框显隐
const dialogVisible = ref(false) // 弹框显隐
const dictObj = ref({
    deviceType: [], // 设备类型
    counterEffect: [], // 反制效果
})
// 注入字典和反制效果选项供FormDiaLog使用
// 注入字典和反制效果选项供 FormDiaLog 使用
provide('dictObj', dictObj)
// 获取列表
applications/drone-command/src/views/detectionCountermeasure/detectionRange/DetectionRangeDialog.vue
@@ -1,86 +1,101 @@
<template>
    <el-dialog
        v-model="visible"
        :title="dialogTitle"
        class="detection-range-dialog"
        destroy-on-close
        @closed="handleClosed"
    >
        <div class="dialog-body">
            <div class="dialog-map">
                <DeviceMapContainer
                    :online-devices="mapDevices"
                    :show-layer-control="false"
                    container-id="detection-range-map"
                />
                <div class="map-toolbar">
                    <button class="map-btn" type="button" aria-label="编辑范围"></button>
                    <button class="map-btn" type="button" aria-label="定位设备"></button>
                </div>
                <div class="map-mini-controls">
                    <button class="map-mini-btn" type="button" aria-label="放大">+</button>
                    <button class="map-mini-btn" type="button" aria-label="缩小">-</button>
                </div>
    <el-dialog v-model="visible" class="ztzf-page-map-view-dialog" :show-close="false" :close-on-click-modal="false"
        destroy-on-close @closed="handleClosed">
        <div class="dialog-container">
            <div class="left-container">
                <div class="leftMap ztzf-cesium" id="detectionRangeMap"></div>
            </div>
            <div class="dialog-form">
                <div class="form-title">{{ formTitle }}</div>
                <el-form ref="formRef" :model="formData" :rules="rules" label-width="84px" :disabled="dialogReadonly">
            <div class="right-container">
                <div class="header">{{ formTitle }}</div>
                <div class="dialog-container" v-if="dialogReadonly">
                    <el-row>
                        <el-col :span="24">
                            <div class="label">侦测设备</div>
                            <div class="val">{{ formData.deviceName || '-' }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备类型</div>
                            <div class="val">{{ getDeviceTypeLabel(formData.deviceType) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备位置</div>
                            <div class="val">{{ formData.deployLocation || '-' }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备编号</div>
                            <div class="val">{{ formData.deviceSn || '-' }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备型号</div>
                            <div class="val">{{ formData.deviceModel || '-' }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">覆盖范围</div>
                            <div class="val">{{ formatRange(formData.effectiveRangeKm) }}</div>
                        </el-col>
                    </el-row>
                </div>
                <el-form v-else ref="formRef" class="dialog-form" :model="formData" :rules="rules" label-width="96px">
                    <el-form-item label="侦测设备" prop="deviceId">
                        <el-select v-model="formData.deviceId" placeholder="请选择" clearable @change="handleDeviceChange">
                            <el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.deviceId" placeholder="请选择" clearable @change="handleDeviceChange">
                            <el-option v-for="item in deviceOptions" :key="item.value" :label="item.label"
                                :value="item.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="设备类型" prop="deviceType">
                        <el-select v-model="formData.deviceType" placeholder="请选择" clearable>
                            <el-option
                                v-for="item in dictObj.deviceType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.deviceType" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.deviceType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="设备位置" prop="deployLocation">
                        <el-input v-model="formData.deployLocation" placeholder="请选择位置">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deployLocation"
                            placeholder="请选择位置">
                            <template #suffix>
                                <span class="suffix-action">地图选点</span>
                            </template>
                        </el-input>
                    </el-form-item>
                    <el-form-item label="设备编号" prop="deviceSn">
                        <el-input v-model="formData.deviceSn" placeholder="请输入" clearable />
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deviceSn" placeholder="请输入"
                            clearable />
                    </el-form-item>
                    <el-form-item label="设备型号" prop="deviceModel">
                        <el-input v-model="formData.deviceModel" placeholder="请输入" clearable />
                        <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deviceModel"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="覆盖范围" prop="effectiveRangeKm">
                        <el-input-number
                            v-model="formData.effectiveRangeKm"
                            :min="0"
                            :precision="2"
                            :controls="false"
                            placeholder="请输入"
                        />
                        <el-input-number class="ztzf-data-cockpit-search-input" v-model="formData.effectiveRangeKm" :min="0" :precision="2" :controls="false"
                            placeholder="请输入" />
                    </el-form-item>
                </el-form>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                    <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting"
                        :disabled="submitting" @click="handleSubmit">
                        保存
                    </el-button>
                </div>
            </div>
        </div>
        <template v-if="!dialogReadonly" #footer>
            <el-button @click="handleCancel">取消</el-button>
            <el-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">保存</el-button>
        </template>
    </el-dialog>
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import { computed, inject, nextTick, ref } from 'vue'
import { ElMessage } from 'element-plus'
import DeviceMapContainer from '@/components/map-container/device-map-container.vue'
import {
    detectionRangeDetailApi,
    detectionRangePageApi,
    detectionRangeSubmitApi,
} from '@/api/detectionCountermeasure/detectionRange'
import { PublicCesium } from '@/utils/cesium/publicCesium'
import * as Cesium from 'cesium'
const initForm = () => ({
    deviceId: '', // 侦测设备ID
@@ -99,17 +114,10 @@
const visible = ref(false) // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const mapDevices = ref([])
const deviceOptions = ref([])
const dialogReadonly = computed(() => dialogMode.value === 'view')
const dialogTitle = computed(() => {
    if (dialogMode.value === 'edit') {
        return '编辑'
    } else if (dialogMode.value === 'view') {
        return '查看'
    }
    return '新增'
})
let viewer
let redPointEntity
const formTitle = computed(() => {
    if (dialogMode.value === 'edit') {
        return '编辑侦测范围'
@@ -123,10 +131,11 @@
const rules = {
    deviceId: [{ required: true, message: '请选择侦测设备', trigger: ['blur', 'change'] }],
    deviceType: [{ required: true, message: '请选择设备类型', trigger: ['blur', 'change'] }],
    deployLocation: [{ required: true, message: '请选择设备位置', trigger: ['blur', 'change'] }],
    effectiveRangeKm: [{ required: true, message: '请输入覆盖范围', trigger: ['blur', 'change'] }],
}
function handleDeviceChange(value) {
function handleDeviceChange (value) {
    const target = deviceOptions.value.find(item => item.value === value)
    const raw = target?.raw
    if (!raw) return
@@ -139,13 +148,24 @@
        raw.deployLocation ?? raw.address ?? raw.location ?? raw.deployAddress ?? formData.value.deployLocation
}
function getDeviceTypeLabel (value) {
    const list = dictObj?.deviceType ?? []
    const target = list.find(item => item.dictKey === value)
    return target?.dictValue ?? value ?? '-'
}
function formatRange (value) {
    if (value == null || value === '') return '-'
    return `${value}km`
}
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -166,13 +186,13 @@
}
// 加载详情
async function loadDetail(id) {
async function loadDetail (id) {
    if (!id) return null
    const res = await detectionRangeDetailApi({ id })
    return res?.data?.data ?? null
}
async function loadDeviceOptions() {
async function loadDeviceOptions () {
    const res = await detectionRangePageApi({
        current: 1,
        size: 200,
@@ -187,14 +207,75 @@
}
// 关闭后重置
function handleClosed() {
function handleClosed () {
    formData.value = initForm()
    viewer = null
    redPointEntity = null
}
function setMapPoint (longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.45),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                pixelOffset: new Cesium.Cartesian2(0, -20),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
function initMap () {
    if (viewer) return
    const publicCesiumInstance = new PublicCesium({
        dom: 'detectionRangeMap',
        flatMode: false,
        terrain: true,
        layerMode: 4,
        boundary: false,
    })
    if (!dialogReadonly.value) {
        publicCesiumInstance.addLeftClickEvent(null, handleMapClick)
    }
    viewer = publicCesiumInstance.getViewer()
}
function handleMapClick (click) {
    const pos = click.position
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    if (!cartesian3) return
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Number(Cesium.Math.toDegrees(carto.longitude).toFixed(6))
    const latitude = Number(Cesium.Math.toDegrees(carto.latitude).toFixed(6))
    formData.value.deployLocation = `${longitude}, ${latitude}`
    setMapPoint(longitude, latitude)
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    visible.value = true
    await nextTick()
    initMap()
    if (dialogMode.value === 'add') {
        await loadDeviceOptions()
        formData.value = initForm()
@@ -212,114 +293,16 @@
    if (currentId && !deviceOptions.value.some(item => item.value === currentId)) {
        deviceOptions.value.unshift({ label: currentName || '-', value: currentId })
    }
    const location = formData.value.deployLocation
    if (location) {
        const [longitude, latitude] = location.split(',').map(item => Number(item.trim()))
        if (!Number.isNaN(longitude) && !Number.isNaN(latitude)) {
            setMapPoint(longitude, latitude)
        }
    }
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.detection-range-dialog {
    :deep(.el-dialog) {
        width: 1100px;
        background: #071833;
        border-radius: 8px;
    }
    :deep(.el-dialog__header) {
        border-bottom: 1px solid rgba(70, 110, 180, 0.35);
    }
}
.dialog-body {
    display: flex;
    gap: 16px;
    min-height: 560px;
}
.dialog-map {
    position: relative;
    flex: 1;
    min-height: 520px;
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid rgba(70, 110, 180, 0.35);
    background: #031024;
}
.map-toolbar {
    position: absolute;
    top: 16px;
    right: 16px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    z-index: 2;
}
.map-btn {
    width: 34px;
    height: 34px;
    border-radius: 6px;
    border: 1px solid rgba(80, 120, 180, 0.6);
    background: rgba(7, 18, 44, 0.9);
    cursor: pointer;
}
.map-mini-controls {
    position: absolute;
    left: 16px;
    bottom: 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    z-index: 2;
}
.map-mini-btn {
    width: 28px;
    height: 28px;
    border-radius: 4px;
    border: 1px solid rgba(80, 120, 180, 0.6);
    background: rgba(7, 18, 44, 0.9);
    color: #cfe3ff;
    cursor: pointer;
}
.dialog-form {
    width: 300px;
    padding: 12px 12px 0;
    border-radius: 8px;
    background: linear-gradient(180deg, rgba(10, 28, 58, 0.95), rgba(6, 16, 34, 0.95));
    border: 1px solid rgba(70, 110, 180, 0.35);
    color: #d7e7ff;
}
.form-title {
    font-size: 14px;
    font-weight: 600;
    margin-bottom: 12px;
    letter-spacing: 1px;
}
:deep(.el-form-item__label) {
    color: rgba(210, 230, 255, 0.85);
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper),
:deep(.el-input-number .el-input__wrapper) {
    background: rgba(7, 20, 44, 0.85);
    border: 1px solid rgba(80, 120, 190, 0.45);
    box-shadow: none;
}
:deep(.el-input__inner) {
    color: #d5e6ff;
}
.suffix-action {
    font-size: 12px;
    color: #7fb6ff;
    cursor: pointer;
}
</style>
<style scoped lang="scss"></style>
applications/drone-command/src/views/detectionCountermeasure/detectionRange/index.vue
@@ -1,4 +1,4 @@
<template>
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search">
            <el-form-item label="名称" prop="deviceName">
@@ -43,24 +43,24 @@
            <div class="ztzf-table-content ztzf-table-content-bg">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="deviceName" show-overflow-tooltip width="140" label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip width="120" label="设备类型">
                    <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                    <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                        </template>
                    </el-table-column>
                    <el-table-column show-overflow-tooltip width="160" label="部署位置">
                    <el-table-column show-overflow-tooltip label="部署位置">
                        <template v-slot="{ row }">
                            {{ formatDeployLocation(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceModel" show-overflow-tooltip width="110" label="型号" />
                    <el-table-column show-overflow-tooltip width="120" label="覆盖范围">
                    <el-table-column prop="deviceModel" show-overflow-tooltip label="型号" />
                    <el-table-column show-overflow-tooltip label="覆盖范围">
                        <template v-slot="{ row }">
                            {{ formatRange(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column show-overflow-tooltip width="150" label="设备编码">
                    <el-table-column show-overflow-tooltip label="设备编码">
                        <template v-slot="{ row }">
                            {{ formatDeviceCode(row) }}
                        </template>
@@ -107,7 +107,7 @@
    size: 10, // 每页大小
})
const searchParams = ref(initSearchParams()) // 查询参数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const total = ref(0) // 总数
const queryParamsRef = ref(null) // 查询表单实例
@@ -160,7 +160,12 @@
// 删除
async function handleDelete(row) {
    await ElMessageBox.confirm('确认删除该条数据吗?', '提示', { type: 'warning' })
    await ElMessageBox.confirm('确认删除该条数据吗?', '提示', {
        type: 'warning',
        customClass: 'ztzf-page-view-message-box',
        confirmButtonClass: 'ztzf-message-box-confirm',
        cancelButtonClass: 'ztzf-message-box-cancel',
    })
    await detectionRangeSubmitApi({ id: row.id, effectiveRangeKm: 0 })
    ElMessage.success('删除成功')
    getList()
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/FormDiaLog.vue
@@ -1,67 +1,120 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false">
        <div v-if="readonly">
<template>
    <el-dialog
        class="ztzf-page-view-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        :close-on-click-modal="false"
        @closed="handleClosed"
        destroy-on-close
    >
        <div v-if="readonly" class="detail-container">
            <div class="detail-title">配置详情</div>
            <el-row>
                <el-col :span="12">
                    <div>起始扇区: {{ formData.startSector }}</div>
                    <div class="label">起始扇区</div>
                    <div class="val">{{ formData.startSector }}</div>
                </el-col>
                <el-col :span="12">
                    <div>终止扇区: {{ formData.endSector }}</div>
                    <div class="label">终止扇区</div>
                    <div class="val">{{ formData.endSector }}</div>
                </el-col>
                <el-col :span="12">
                    <div>搜索模式: {{ formData.searchMode }}</div>
                    <div class="label">搜索模式</div>
                    <div class="val">{{ formData.searchMode }}</div>
                </el-col>
                <el-col :span="12">
                    <div>雷达频点: {{ formData.radarFreq }}</div>
                    <div class="label">雷达频点</div>
                    <div class="val">{{ formData.radarFreq }}</div>
                </el-col>
                <el-col :span="12">
                    <div>伺服模式: {{ formData.servoMode }}</div>
                    <div class="label">伺服模式</div>
                    <div class="val">{{ formData.servoMode }}</div>
                </el-col>
                <el-col :span="12">
                    <div>伺服数据模式: {{ formData.servoDataMode }}</div>
                    <div class="label">伺服数据模式</div>
                    <div class="val">{{ formData.servoDataMode }}</div>
                </el-col>
                <el-col :span="12">
                    <div>轴选择: {{ formData.axisSelect }}</div>
                    <div class="label">轴选择</div>
                    <div class="val">{{ formData.axisSelect }}</div>
                </el-col>
                <el-col :span="12">
                    <div>伺服转速度: {{ formData.servoTurnSpeed }}</div>
                    <div class="label">伺服转速度</div>
                    <div class="val">{{ formData.servoTurnSpeed }}</div>
                </el-col>
            </el-row>
        </div>
        <el-form v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" label-width="120px">
        <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="120px" class="dialog-form">
            <el-row>
                <el-col :span="12">
                    <el-form-item label="起始扇区" prop="startSector">
                        <el-input v-model="formData.startSector" maxlength="50" placeholder="请输入" clearable />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.startSector"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="终止扇区" prop="endSector">
                        <el-input v-model="formData.endSector" maxlength="50" placeholder="请输入" clearable />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.endSector"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="搜索模式" prop="searchMode">
                        <el-select v-model="formData.searchMode" placeholder="请选择" clearable>
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.searchMode"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option v-for="item in searchModeOptions" :key="item.value" :label="item.label" :value="item.value" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="雷达频点" prop="radarFreq">
                        <el-input v-model="formData.radarFreq" maxlength="50" placeholder="请输入" clearable />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.radarFreq"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="伺服模式" prop="servoMode">
                        <el-select v-model="formData.servoMode" placeholder="请选择" clearable>
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.servoMode"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option v-for="item in servoModeOptions" :key="item.value" :label="item.label" :value="item.value" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="伺服数据模式" prop="servoDataMode">
                        <el-select v-model="formData.servoDataMode" placeholder="请选择" clearable>
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.servoDataMode"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in servoDataModeOptions"
                                :key="item.value"
@@ -73,22 +126,41 @@
                </el-col>
                <el-col :span="12">
                    <el-form-item label="轴选择" prop="axisSelect">
                        <el-select v-model="formData.axisSelect" placeholder="请选择" clearable>
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.axisSelect"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option v-for="item in axisSelectOptions" :key="item.value" :label="item.label" :value="item.value" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="伺服转速度" prop="servoTurnSpeed">
                        <el-input v-model="formData.servoTurnSpeed" maxlength="50" placeholder="请输入" clearable />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.servoTurnSpeed"
                            maxlength="50"
                            placeholder="请输入"
                            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 color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
            <el-button
                color="#284FE3"
                v-if="!readonly"
                type="primary"
                :loading="submitting"
                :disabled="submitting"
                @click="handleSubmit"
            >
                保存
            </el-button>
        </template>
    </el-dialog>
@@ -97,7 +169,7 @@
<script setup>
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDeviceConfigDetailApi, fwDeviceConfigSubmitApi } from './deviceAppConfigApi'
import { fwDeviceConfigSubmitApi } from './deviceAppConfigApi'
import { fieldRules } from '@ztzf/utils'
const configKeys = [
@@ -144,7 +216,7 @@
    { label: '扇区', value: '扇区' },
    { label: '定位', value: '定位' },
]
const servoDataModeOptions = [{ label: '编码器', value: '编码器' }]
const servoDataModeOptions = [{ label: '编码模式', value: '编码模式' }]
const axisSelectOptions = [{ label: '方位', value: '方位' }]
const rules = {
@@ -171,6 +243,10 @@
    visible.value = false
}
function handleClosed() {
    formData.value = initForm()
}
// 提交更新
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/index.vue
@@ -1,59 +1,74 @@
<template>
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams">
            <el-row :gutter="16">
                <el-col :span="4">
                    <el-form-item label="设备名称" prop="deviceName">
                        <el-input v-model="searchParams.deviceName" placeholder="请输入" clearable @clear="handleSearch" />
                    </el-form-item>
                </el-col>
                <el-col :span="4">
                    <el-form-item label="设备类型" prop="deviceType">
                        <el-select v-model="searchParams.deviceType" placeholder="请选择" clearable @change="handleSearch">
                            <el-option
                                v-for="item in dictObj.deviceType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </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 ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search">
            <el-form-item label="设备名称" prop="deviceName">
                <el-input
                    class="ztzf-data-cockpit-search-input"
                    v-model="searchParams.deviceName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
                <el-select
                    class="ztzf-data-cockpit-select"
                    popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.deviceType"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.deviceType"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item class="history-search-actions">
                <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button>
            </el-form-item>
        </el-form>
        <el-table v-loading="loading" :data="list">
            <el-table-column type="index" width="60" label="序号" />
            <el-table-column prop="deviceType" label="设备类型">
                <template v-slot="{ row }">
                    {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                </template>
            </el-table-column>
            <el-table-column prop="deviceModel" label="设备型号" />
            <el-table-column prop="deviceName" label="设备名称" />
            <el-table-column prop="deviceSn" label="设备编码" />
            <el-table-column prop="deployLocation" label="部署位置" />
            <el-table-column prop="manufacturer" label="厂商" />
            <el-table-column prop="contactPhone" label="联系方式" />
            <el-table-column prop="sceneName" label="所属场景" />
            <el-table-column label="操作">
                <template v-slot="{ row }">
                    <el-link @click="openForm('edit', row)" type="warning">配置接口</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 class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
            <div class="ztzf-table-content ztzf-table-content-bg">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" width="64" show-overflow-tooltip label="序号" />
                    <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceModel" show-overflow-tooltip label="设备型号" />
                    <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                    <el-table-column prop="deviceSn" show-overflow-tooltip label="设备编码" />
                    <el-table-column prop="deployLocation" show-overflow-tooltip label="部署位置" />
                    <el-table-column prop="manufacturer" show-overflow-tooltip label="厂商" />
                    <el-table-column prop="contactPhone" show-overflow-tooltip label="联系方式" />
                    <el-table-column prop="sceneName" show-overflow-tooltip label="所属场景" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('edit', row)" type="warning">配置接口</el-link>
                        </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>
        <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" />
@@ -61,6 +76,7 @@
</template>
<script setup>
import { RefreshRight, Search } from '@element-plus/icons-vue'
import { nextTick, onMounted, provide, ref } from 'vue'
import FormDiaLog from './FormDiaLog.vue'
import { getDictionaryByCode } from '@/api/system/dictbiz'
@@ -77,7 +93,7 @@
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/FormDiaLog.vue
@@ -1,56 +1,101 @@
<template>
    <el-dialog v-model="visible" :title="titleEnum[dialogMode]">
        <div v-if="readonly">
<template>
    <el-dialog
        class="ztzf-page-view-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        destroy-on-close
        :close-on-click-modal="false"
        @closed="handleClosed"
    >
        <div v-if="readonly" class="detail-container">
            <div class="detail-title">调度详情</div>
            <el-row>
                <el-col :span="12">
                    <div>设备名称: {{ formData.deviceName }}</div>
                    <div class="label">设备名称</div>
                    <div class="val">{{ formData.deviceName }}</div>
                </el-col>
                <el-col :span="12">
                    <div>设备编码: {{ formData.deviceSn }}</div>
                    <div class="label">设备编码</div>
                    <div class="val">{{ formData.deviceSn }}</div>
                </el-col>
                <el-col :span="12">
                    <div>场景: {{ formData.defenseSceneName }}</div>
                    <div class="label">场景</div>
                    <div class="val">{{ formData.defenseSceneName }}</div>
                </el-col>
                <el-col :span="12">
                    <div>区域: {{ formData.areaDivideName }}</div>
                    <div class="label">区域</div>
                    <div class="val">{{ formData.areaDivideName }}</div>
                </el-col>
                <el-col :span="12">
                    <div>调度人员: {{ formData.dispatchUser }}</div>
                    <div class="label">调度人员</div>
                    <div class="val">{{ formData.dispatchUser }}</div>
                </el-col>
                <el-col :span="12">
                    <div>调度位置: {{ formData.longitude }}, {{ formData.latitude }}</div>
                    <div class="label">调度位置</div>
                    <div class="val">{{ formData.longitude }}, {{ formData.latitude }}</div>
                </el-col>
                <el-col :span="12">
                    <div>调度日期: {{ formData.dispatchStartTime }} ~ {{ formData.dispatchEndTime }}</div>
                    <div class="label">调度日期</div>
                    <div class="val">{{ formData.dispatchStartTime }} ~ {{ formData.dispatchEndTime }}</div>
                </el-col>
                <el-col :span="24">
                    <div>调度内容: {{ formData.dispatchContent }}</div>
                    <div class="label">调度内容</div>
                    <div class="val">{{ formData.dispatchContent }}</div>
                </el-col>
                <el-col :span="24">
                    <div>调度结果: {{ formData.dispatchResult }}</div>
                    <div class="label">调度结果</div>
                    <div class="val">{{ formData.dispatchResult }}</div>
                </el-col>
                <el-col :span="24">
                    <div>处理措施: {{ formData.disposeMeasure }}</div>
                    <div class="label">处理措施</div>
                    <div class="val">{{ formData.disposeMeasure }}</div>
                </el-col>
            </el-row>
        </div>
        <el-form v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" label-width="100px">
        <el-form
            v-else
            ref="formRef"
            :model="formData"
            :rules="rules"
            :disabled="readonly"
            label-width="100px"
            class="dialog-form"
        >
            <el-row>
                <el-col :span="12">
                    <el-form-item label="设备" prop="deviceId">
                        <el-select v-model="formData.deviceId" placeholder="请选择" clearable @change="handleDeviceChange">
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.deviceId"
                            placeholder="请选择"
                            clearable
                            @change="handleDeviceChange"
                        >
                            <el-option v-for="item in deviceList" :key="item.id" :label="item.deviceName" :value="item.id" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="设备编码" prop="deviceSn">
                        <el-input v-model="formData.deviceSn" disabled placeholder="选择设备后自动带出" />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.deviceSn"
                            disabled
                            placeholder="选择设备后自动带出"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="场景" prop="defenseSceneId">
                        <el-select v-model="formData.defenseSceneId" placeholder="请选择" clearable @change="handleSceneChange">
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.defenseSceneId"
                            placeholder="请选择"
                            clearable
                            @change="handleSceneChange"
                        >
                            <el-option v-for="item in sceneList" :key="item.id" :label="item.sceneName" :value="item.id" />
                        </el-select>
                    </el-form-item>
@@ -58,6 +103,8 @@
                <el-col :span="12">
                    <el-form-item label="区域" prop="areaDivideId">
                        <el-select
                            class="ztzf-data-cockpit-select"
                            popper-class="ztzf-data-cockpit-select-popper"
                            v-model="formData.areaDivideId"
                            placeholder="请先选择场景"
                            clearable
@@ -70,12 +117,20 @@
                </el-col>
                <el-col :span="12">
                    <el-form-item label="调度人员" prop="dispatchUser">
                        <el-input v-model="formData.dispatchUser" maxlength="50" placeholder="请输入" clearable />
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.dispatchUser"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="调度日期" prop="dispatchDateRange">
                        <el-date-picker
                            class="ztzf-data-cockpit-date-picker"
                            popper-class="ztzf-data-cockpit-date-picker-popper"
                            v-model="formData.dispatchDateRange"
                            type="datetimerange"
                            range-separator="至"
@@ -87,7 +142,7 @@
                </el-col>
                <el-col :span="24">
                    <el-form-item label="调度位置" prop="longitude">
<!--                        <el-button @click="selectLocation" style="width: 100%">-->
                        <!--                        <el-button @click="selectLocation" style="width: 100%">-->
                        <el-button style="width: 100%">
                            {{
                                formData.longitude && formData.latitude ? `${formData.longitude}, ${formData.latitude}` : '选择区域后自动带出'
@@ -98,6 +153,7 @@
                <el-col :span="24">
                    <el-form-item label="调度内容" prop="dispatchContent">
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.dispatchContent"
                            type="textarea"
                            :rows="3"
@@ -110,6 +166,7 @@
                <el-col :span="24">
                    <el-form-item label="调度结果" prop="dispatchResult">
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.dispatchResult"
                            type="textarea"
                            :rows="3"
@@ -122,6 +179,7 @@
                <el-col :span="24">
                    <el-form-item label="处理措施" prop="disposeMeasure">
                        <el-input
                            class="ztzf-data-cockpit-search-input"
                            v-model="formData.disposeMeasure"
                            type="textarea"
                            :rows="3"
@@ -135,9 +193,16 @@
        </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 color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
            <el-button
                color="#284FE3"
                v-if="!readonly"
                type="primary"
                :loading="submitting"
                :disabled="submitting"
                @click="handleSubmit"
            >
                保存
            </el-button>
        </template>
@@ -152,7 +217,7 @@
</template>
<script setup>
import { computed, nextTick, ref, inject } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fwTaskScheduleDetailApi, fwTaskScheduleSubmitApi } from './taskScheduleApi'
import { fwAreaDivideListApi } from '@/views/areaManage/partition/partitionApi'
@@ -249,6 +314,10 @@
    visible.value = false
}
function handleClosed() {
    formData.value = initForm()
}
// 提交新增/编辑
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/index.vue
@@ -1,9 +1,10 @@
<template>
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search">
            <el-form-item label="设备名称" prop="deviceId">
                <el-select
                    class="ztzf-data-cockpit-search-input"
                    class="ztzf-data-cockpit-select"
                    popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.deviceId"
                    placeholder="请选择"
                    clearable
@@ -13,9 +14,11 @@
                    <el-option v-for="item in deviceList" :key="item.id" :label="item.deviceName" :value="item.id" />
                </el-select>
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
                <el-select
                    class="ztzf-data-cockpit-search-input"
                    class="ztzf-data-cockpit-select"
                    popper-class="ztzf-data-cockpit-select-popper"
                    v-model="searchParams.deviceType"
                    placeholder="请选择"
                    clearable
@@ -30,6 +33,7 @@
                    />
                </el-select>
            </el-form-item>
            <el-form-item label="设备编号" prop="deviceSn">
                <el-input
                    class="ztzf-data-cockpit-search-input"
@@ -48,7 +52,7 @@
        <div class="ztzf-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -59,17 +63,17 @@
                    <el-table-column prop="deviceName" show-overflow-tooltip width="140" label="设备名称" />
                    <el-table-column prop="deviceSn" show-overflow-tooltip width="140" label="设备编码" />
                    <!--                    <el-table-column prop="deviceStatus" show-overflow-tooltip width="100" label="设备状态">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceStatus, dictObj.deviceStatus) }}
                        </template>
                    </el-table-column>-->
                            <template v-slot="{ row }">
                                {{ getDictLabel(row.deviceStatus, dictObj.deviceStatus) }}
                            </template>
                        </el-table-column>-->
                    <el-table-column prop="areaName" show-overflow-tooltip width="120" label="区域" />
                    <el-table-column prop="defenseZoneName" show-overflow-tooltip width="120" label="场景" />
                    <el-table-column show-overflow-tooltip width="200" label="调度位置">
                    <el-table-column show-overflow-tooltip label="调度位置">
                        <template v-slot="{ row }">{{ row.longitude }}, {{ row.latitude }}</template>
                    </el-table-column>
                    <el-table-column prop="dispatchUser" show-overflow-tooltip width="100" label="调度人员" />
                    <el-table-column show-overflow-tooltip width="180" label="调度日期">
                    <el-table-column show-overflow-tooltip label="调度日期">
                        <template v-slot="{ row }">
                            {{ formatDispatchDate(row) }}
                        </template>
@@ -110,7 +114,7 @@
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref, provide } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwTaskSchedulePageApi, fwTaskScheduleRemoveApi } from './taskScheduleApi'
@@ -130,7 +134,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
applications/drone-command/src/views/job/components/JobRelatedEvents.vue
@@ -144,7 +144,7 @@
    params.value.ai_types = val
  handleSearch()
}
const loading = ref(false)
const loading = ref(true)
const list = ref()
const params = ref({
    way_line_job_info_id: null,
applications/drone-command/src/views/job/components/TaskIntermediateContent/AddTask.vue
@@ -151,7 +151,7 @@
})
const isShowAddTask = defineModel('show')
const loading = ref(false)
const loading = ref(true)
// 禁用当天之前的日期
const disabledDate = time => {
applications/drone-command/src/views/job/components/executeNestDetails.vue
@@ -79,7 +79,7 @@
const deviseName = ref(null)
const devisejobInfoId = ref('')
const devisebatchNo = ref(null)
const loading = ref(false)
const loading = ref(true)
const isShowContinuingToFly = code => {
    return ContinuingToFly.find(i => i.code === code)
}
applications/drone-command/src/views/layerManagement/components/leftList.vue
@@ -123,7 +123,7 @@
  dkbh: '',
  patchesName: '',
});
const loading = ref(false);
const loading = ref(true);
const treeData = ref([]);
const treeAllData = ref([]);
// 存储过滤后显示的节点ID(返回true的节点)
applications/drone-command/src/views/layerManagement/components/rightEdit.vue
@@ -156,7 +156,7 @@
  geo_data: '',
});
const loading = ref(false);
const loading = ref(true);
const detailData = ref(null);
const fenceArea = ref(0);
detailData.value = layerParams.value.editDetailData;
applications/drone-command/src/views/recordManage/alarmRecords/index.vue
@@ -56,7 +56,7 @@
        </el-form>
        <div class="ztzf-table-toolbar">
            <el-button :icon="Download" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
            <el-button :icon="Download" color="#1A2652" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -64,18 +64,18 @@
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="droneName" show-overflow-tooltip width="140" label="无人机名称" />
                    <el-table-column prop="droneSerialNo" show-overflow-tooltip width="160" label="无人机序列号" />
                    <el-table-column prop="alarmTime" show-overflow-tooltip width="160" label="告警时间" />
                    <el-table-column prop="areaName" show-overflow-tooltip width="120" label="区域" />
                    <el-table-column prop="triggerReason" show-overflow-tooltip width="160" label="触发原因" />
                    <el-table-column prop="deviceName" show-overflow-tooltip width="160" label="侦测反制设备" />
                    <el-table-column prop="droneType" show-overflow-tooltip width="120" label="设备类型">
                    <el-table-column prop="droneSerialNo" show-overflow-tooltip label="无人机序列号" />
                    <el-table-column prop="alarmTime" show-overflow-tooltip label="告警时间" />
                    <el-table-column prop="areaName" show-overflow-tooltip label="区域" />
                    <el-table-column prop="triggerReason" show-overflow-tooltip label="触发原因" />
                    <el-table-column prop="deviceName" show-overflow-tooltip label="侦测反制设备" />
                    <el-table-column prop="droneType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ getDeviceTypeName(row.droneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceSn" show-overflow-tooltip width="140" label="设备编码" />
                    <el-table-column show-overflow-tooltip width="120" label="设备状态">
                    <el-table-column prop="deviceSn" show-overflow-tooltip label="设备编码" />
                    <el-table-column show-overflow-tooltip label="设备状态">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.deviceStatus, dictObj.deviceStatus) }}
                        </template>
@@ -116,7 +116,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
applications/drone-command/src/views/recordManage/historyTracks/TrajectoryDiaLog.vue
@@ -1,32 +1,59 @@
<template>
    <el-dialog v-model="visible" :title="dialogTitle" @closed="handleClosed" destroy-on-close>
        <div class="bodyBox">
            <div class="leftMap ztzf-cesium" id="leftMapContainer"></div>
            <div class="rightInfo">
                <div>
                    <div>无人机名称:{{ formData.droneName }}</div>
                    <div>序列号: {{ formData.serialNo }}</div>
                    <div>告警时间: {{ formData.alarmTime }}</div>
                    <div>触发原因: {{ formData.triggerType }}</div>
                    <div>停留时长: {{ formData.stayDuration }}</div>
<template>
    <el-dialog class="ztzf-page-map-view-dialog" v-model="visible" :show-close="false"
        :close-on-click-modal="false" @closed="handleClosed" destroy-on-close>
        <div class="dialog-container">
            <div class="left-container">
                <div class="leftMap ztzf-cesium" id="leftMapContainer"></div>
            </div>
            <div class="right-container">
                <div class="header">{{ dialogTitle }}</div>
                <div class="dialog-container">
                    <div class="detail-title">轨迹信息</div>
                    <el-row>
                        <el-col :span="24">
                            <div class="label">无人机名称</div>
                            <div class="val">{{ formData.droneName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">序列号</div>
                            <div class="val">{{ formData.serialNo }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">告警时间</div>
                            <div class="val">{{ formData.alarmTime }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">触发原因</div>
                            <div class="val">{{ formData.triggerType }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">停留时长</div>
                            <div class="val">{{ formData.stayDuration }}</div>
                        </el-col>
                    </el-row>
                    <div class="detail-title">轨迹点</div>
                    <div class="ztzf-table-container">
                        <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 label="经纬度">
                                    <template #default="{ row }">{{ row.longitude }},{{ row.latitude }}</template>
                                </el-table-column>
                                <el-table-column prop="createTime" label="时间" />
                            </el-table>
                        </div>
                    </div>
                </div>
                <el-table :data="list">
                    <el-table-column type="index" width="60" label="序号" />
                    <el-table-column label="经纬度">
                        <template #default="{ row }">{{ row.longitude }},{{ row.latitude }}</template>
                    </el-table-column>
                    <el-table-column prop="createTime" label="时间" />
                </el-table>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                </div>
            </div>
        </div>
        <template #footer>
            <el-button @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
        </template>
    </el-dialog>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { fwDroneFlightRecordDetailApi } from '@/views/recordManage/historyTracks/fwDroneFlightRecord'
import { fwDroneFlightRecordDetailPageApi } from '@/views/recordManage/historyTracks/flyTrajectory'
import { PublicCesium } from '@/utils/cesium/publicCesium'
@@ -57,12 +84,12 @@
})
// 关闭弹框
function handleCancel() {
function handleCancel () {
    visible.value = false
}
// 加载详情
async function loadDetail() {
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwDroneFlightRecordDetailApi({ id: formData.value.id })
    const res1 = await fwDroneFlightRecordDetailPageApi({ flightRecordId: formData.value.id })
@@ -72,11 +99,11 @@
}
// 角度转c3
function degreesToC3({ longitude, latitude, height = 0 }) {
function degreesToC3 ({ longitude, latitude, height = 0 }) {
    return Cesium.Cartesian3.fromDegrees(longitude, latitude, height)
}
// 渲染航线
function drawFlightPath() {
function drawFlightPath () {
    const positions = list.value.map(p => degreesToC3(p))
    droneLineEntity && geometricSource.entities.remove(droneLineEntity)
    const color = Cesium.Color.fromCssColorString('#00D690')
@@ -92,12 +119,17 @@
}
// 关闭后重置
function handleClosed() {
function handleClosed () {
    formData.value = initForm()
    list.value = []
    viewer = null
    geometricSource = null
    droneLineEntity = null
}
let viewer, geometricSource, droneLineEntity
function initMap() {
function initMap () {
    if (viewer) return
    const publicCesiumInstance = new PublicCesium({
        dom: 'leftMapContainer',
        flatMode: false,
@@ -111,12 +143,11 @@
}
// 打开弹框
async function open({ mode, row } = {}) {
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    visible.value = true
    await nextTick(() => {
        initMap()
    })
    await nextTick()
    initMap()
    if (dialogMode.value === 'add') {
        formData.value = initForm()
    } else {
@@ -127,16 +158,5 @@
defineExpose({ open })
</script>
<style scoped lang="scss">
.bodyBox {
    display: flex;
    .rightInfo {
        width: 50%;
        height: 500px;
    }
    .leftMap {
        width: 50%;
        height: 500px;
    }
}
</style>
<style scoped lang="scss"></style>
applications/drone-command/src/views/recordManage/historyTracks/index.vue
@@ -39,7 +39,7 @@
        </el-form>
        <div class="ztzf-table-toolbar">
            <el-button :icon="Download" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
            <el-button :icon="Download" color="#1A2652" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button>
        </div>
        <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
@@ -47,11 +47,11 @@
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="droneName" show-overflow-tooltip width="140" label="无人机名称" />
                    <el-table-column prop="serialNo" show-overflow-tooltip width="160" label="序列号" />
                    <el-table-column prop="alarmTime" show-overflow-tooltip width="160" label="告警时间" />
                    <el-table-column prop="areaName" show-overflow-tooltip width="120" label="区域" />
                    <el-table-column prop="triggerType" show-overflow-tooltip width="160" label="触发原因" />
                    <el-table-column show-overflow-tooltip width="140" label="停留时长">
                    <el-table-column prop="serialNo" show-overflow-tooltip label="序列号" />
                    <el-table-column prop="alarmTime" show-overflow-tooltip label="告警时间" />
                    <el-table-column prop="areaName" show-overflow-tooltip label="区域" />
                    <el-table-column prop="triggerType" show-overflow-tooltip label="触发原因" />
                    <el-table-column show-overflow-tooltip label="停留时长">
                        <template v-slot="{ row }">
                            {{ formatStayDuration(row.stayDuration) }}
                        </template>
@@ -95,7 +95,7 @@
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(false) // 列表加载中
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const dateRange = ref([])
applications/drone-command/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/drone-command/src/views/tickets/ticket.vue
@@ -509,7 +509,7 @@
const detailVisible = ref(false)
const currentDetail = ref({})
const departmentUsers = ref({})
const loading = ref(false)
const loading = ref(true)
const globalCounts = ref({})
const mapLoaded = ref(false)
const isFetching = ref(false)
applications/drone-command/src/views/tickets/ticketComponent/DispatchDialog.vue
@@ -88,7 +88,7 @@
});
const emit = defineEmits(['update:modelValue', 'dispatch-success', 'dispatch-error']);
const formRef = ref(null); // 表单引用
const loading = ref(false);
const loading = ref(true);
const form = ref({
  department: '',
  handler: ''
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,52 +1,55 @@
<template>
    <div class="call-container">
      <div class="content-wrapper">
        <div class="input-group">
            <div class="input-item">
                <span class="input-label">我的 userId:</span>
                <input v-model="uid" class="input-field" />
    <div class="call-container" :class="{'incoming-call': state === 'ringing'} ">
        <div class="content-wrapper">
            <!--        <div class="input-group">
                    <div class="input-item">
                        <span class="input-label">我的 userId:</span>
                        <input v-model="uid" class="input-field" />
                    </div>
                    <div class="input-item">
                        <span class="input-label">对方 userId:</span>
                        <input v-model="peerUid" class="input-field" />
                    </div>
                </div>-->
            <div class="button-group" v-if="false">
                <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
            </div>
            <div class="input-item">
                <span class="input-label">对方 userId:</span>
                <input v-model="peerUid" class="input-field" />
            </div>
        </div>
        <div class="button-group">
            <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
        </div>
      <!--  头像-->
            <!--  头像-->
            <div class="contactAvatar">
                <img :src="defaultAvatar" alt=""></img>
            </div>
        <div class="button-group">
            <button @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button>
            <button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept">接听</button>
            <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject">拒绝</button>
            <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')" class="btn btn-hangup">挂断</button>
            <div class="status-group">
                <strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong>
                <strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong>
                <!--    <strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>-->
                <strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong>
            </div>
            <div class="button-group">
                <button v-if="state === 'idle' && !receiveParameters" @click="requestCall" :disabled="!connected" class="btn btn-call">呼叫</button>
                <button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button>
                <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button>
                <button v-if="state==='calling' || state==='in-call'" @click="hangup" class="btn btn-hangup">挂断</button>
            </div>
        </div>
        <div class="status-group">
            <strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>
            <strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong>
        </div>
      </div>
      <div class="tip-message">
        <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
      </div>
      <div class="log-container">
        <div class="log-box">
            <div class="log-title">日志信息:</div>
            <pre class="log-content">{{ logText }}</pre>
        </div>
      </div>
      <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
        <!--    <div class="tip-message">
            <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
            </div>-->
        <!--    <div class="log-container">
            <div class="log-box">
                <div class="log-title">日志信息:</div>
                <pre class="log-content">{{ logText }}</pre>
            </div>
            </div>-->
        <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
    </div>
</template>
@@ -57,25 +60,69 @@
/** ✅ 你的 WS 地址前缀(后面拼 userId) */
const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
  // 解析URL参数
  const parseUrlParams = () => {
    const url = new URL(window.location.href)
    const paramsStr = url.searchParams.get('params')
    if (paramsStr) {
      try {
        const params = JSON.parse(decodeURIComponent(paramsStr))
        if (params.peerUid) {
          peerUid.value = String(params.peerUid)
          log('🔗 从URL参数获取peerUid:', params.peerUid)
        }
      } catch (e) {
        log('❌ 解析URL参数失败:', String(e))
      }
    }
  }
// 解析URL参数
const parseUrlParams = () => {
    // 1. 优先从route.query.voiceparams获取参数
    if (route.query && route.query.voiceparams) {
        console.log('从route.query.voiceparams获取参数:', route.query.voiceparams);
        try {
            // 解码并解析voiceparams参数
            const params = JSON.parse(decodeURIComponent(route.query.voiceparams));
            console.log('解析后的voiceparams:', params);
            if (params.peerUid) {
                peerUid.value = String(params.peerUid);
                log('🔗 从voiceparams获取peerUid:', params.peerUid);
            }
            // 如果是来电类型,直接触发来电状态
            if (params.type === 'incoming') {
                log('📞 voiceparams检测到来电,直接进入来电状态');
                incomingFrom = String(params.from);
                peerUid.value = incomingFrom;
                acceptedByMe = false;
                offeredByPeer = false;
                state.value = 'ringing';
                log('📞 收到来电 call,来自', incomingFrom);
                // 开始来电提示(铃声和震动)
                playRingtone();
                startVibration();
            }
        } catch (e) {
            log('❌ 解析voiceparams参数失败:', String(e));
        }
    }
    // 2. 同时保留原有的URL参数解析逻辑,作为备选
    const url = new URL(window.location.href);
    const paramsStr = url.searchParams.get('params');
    if (paramsStr) {
        try {
            const params = JSON.parse(decodeURIComponent(paramsStr));
            if (params.peerUid) {
                peerUid.value = String(params.peerUid);
                log('🔗 从URL参数获取peerUid:', params.peerUid);
            }
        } catch (e) {
            log('❌ 解析URL参数失败:', String(e));
        }
    }
}
  const uid = ref('3')
  const peerUid = ref('2')
// 从URL参数或本地存储获取用户ID
const getUserIdFromStorage = () => {
    const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId')
    if (userId) {
        log('🔗 从本地存储获取userId:', userId)
        return userId
    }
    return '3' // 默认值
}
const uid = ref('3')
const peerUid = ref('2')
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
@@ -112,59 +159,130 @@
    const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
    const s = String(seconds.value % 60).padStart(2, '0')
    return `${m}:${s}`
  })
  function startTimer() {
})
function startTimer() {
    stopTimer()
    seconds.value = 0
    timer = setInterval(() => seconds.value++, 1000)
  }
  function stopTimer() {
}
function stopTimer() {
    if (timer) clearInterval(timer)
    timer = null
  }
  // 心跳
  let pingTimer = null
  function startPing() {
}
// 心跳
let pingTimer = null
function startPing() {
    stopPing()
    // 每 25 秒发一次 ping(你后端有 PING/PONG)
    pingTimer = setInterval(() => {
      if (connected.value) send('ping', 'system', null)
        if (connected.value) send('ping', 'system', null)
    }, 25000)
  }
  function stopPing() {
}
// 来电提示相关
let ringtoneAudio = null
let vibrationTimer = null
// 播放来电铃声
function playRingtone() {
    // 创建音频对象(使用默认的系统提示音)
    ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA==');
    ringtoneAudio.loop = true;
    ringtoneAudio.volume = 0.5;
    try {
        ringtoneAudio.play();
        log('🔊 开始播放来电铃声')
    } catch (e) {
        log('❌ 播放来电铃声失败:', String(e))
    }
}
// 停止来电铃声
function stopRingtone() {
    if (ringtoneAudio) {
        try {
            ringtoneAudio.pause();
            ringtoneAudio.currentTime = 0;
            ringtoneAudio = null;
            log('🔇 停止播放来电铃声')
        } catch (e) {
            log('❌ 停止来电铃声失败:', String(e))
        }
    }
}
// 开始震动
function startVibration() {
    // 检查浏览器是否支持震动 API
    if ('vibrate' in navigator) {
        // 震动模式:震动1秒,暂停0.5秒,重复
        const vibrationPattern = [1000, 500];
        try {
            navigator.vibrate(vibrationPattern);
            vibrationTimer = setInterval(() => {
                navigator.vibrate(vibrationPattern);
            }, 1500); // 每1.5秒重复一次震动模式
            log('📳 开始震动提示')
        } catch (e) {
            log('❌ 震动失败:', String(e))
        }
    } else {
        log('ℹ️ 当前浏览器不支持震动功能')
    }
}
// 停止震动
function stopVibration() {
    if (vibrationTimer) {
        clearInterval(vibrationTimer);
        vibrationTimer = null;
    }
    if ('vibrate' in navigator) {
        try {
            navigator.vibrate(0); // 停止震动
            log('📳 停止震动提示')
        } catch (e) {
            log('❌ 停止震动失败:', String(e))
        }
    }
}
function stopPing() {
    if (pingTimer) clearInterval(pingTimer)
    pingTimer = null
  }
  function log(...args) {
}
function log(...args) {
    const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
    logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
    console.log(...args)
  }
  /**
   * ✅ 严格按后端 ChatMessage 字段发:
   * { type, from, to, payload, timestamp }
   * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
   */
  function send(type, to, payload) {
}
/**
 * ✅ 严格按后端 ChatMessage 字段发:
 * { type, from, to, payload, timestamp }
 * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
 */
function send(type, to, payload) {
    if (!type) return
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    const msg = {
      type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
      from: String(uid.value),
      to: String(to),
      payload: payload ?? null,
      timestamp: Date.now(),
        type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
        from: String(uid.value),
        to: String(to),
        payload: payload ?? null,
        timestamp: Date.now(),
    }
    ws.send(JSON.stringify(msg))
  }
  /* ---------------- WebRTC ---------------- */
  function createPC() {
}
/* ---------------- WebRTC ---------------- */
function createPC() {
    pc = new RTCPeerConnection({
        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    })
@@ -244,6 +362,8 @@
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
console.log('1cesss',msg);
    if (!msg) return
    const t = (msg.type || '').toString()
@@ -269,6 +389,11 @@
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        // 开始来电提示(铃声和震动)
        playRingtone()
        startVibration()
        return
    }
@@ -284,356 +409,518 @@
        cleanup(false)
        return
    }
    if (t === 'offer') {
      // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
      offeredByPeer = true
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      if (!acceptedByMe) {
        state.value = 'ringing'
        log('📩 收到 offer(但未接听),已等待用户点击接听')
        // 暂存 offer 到一个变量
        lastOfferSdp = msg.payload?.sdp
        // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
        offeredByPeer = true
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        if (!acceptedByMe) {
            state.value = 'ringing'
            log('📩 收到 offer(但未接听),已等待用户点击接听')
            // 暂存 offer 到一个变量
            lastOfferSdp = msg.payload?.sdp
            return
        }
        // 已接听:直接 answer
        await answerOffer(msg.payload?.sdp, incomingFrom)
        return
      }
      // 已接听:直接 answer
      await answerOffer(msg.payload?.sdp, incomingFrom)
      return
    }
    if (t === 'answer') {
      log('✅ 收到 answer,通话建立')
      if (!pc) createPC()
      await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
      await flushCandidates()
      state.value = 'in-call'
      startTimer()
      return
        log('✅ 收到 answer,通话建立')
        if (!pc) createPC()
        await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
        await flushCandidates()
        state.value = 'in-call'
        startTimer()
        return
    }
    if (t === 'candidate') {
      const c = msg.payload?.candidate
      if (!c) return
      if (pc && pc.remoteDescription) {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } else {
        pendingCandidates.push(c)
      }
      return
        const c = msg.payload?.candidate
        if (!c) return
        if (pc && pc.remoteDescription) {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } else {
            pendingCandidates.push(c)
        }
        return
    }
    if (t === 'hangup') {
      log('📴 对方挂断')
      cleanup(false)
      return
        log('📴 对方挂断')
        cleanup(false)
        return
    }
    if (t === 'pong') {
      // 心跳响应
      // log('pong')
      return
        // 心跳响应
        // log('pong')
        return
    }
  }
  // 暂存 offer(用于“先点接听再 answer”)
  let lastOfferSdp = null
  /* ---------------- 业务操作 ---------------- */
  function connectWS() {
}
// 暂存 offer(用于“先点接听再 answer”)
let lastOfferSdp = null
/* ---------------- 业务操作 ---------------- */
function connectWS() {
    // 确保获取最新的用户ID
    uid.value = getUserIdFromStorage()
    if (ws) ws.close()
    ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
    ws.onopen = () => {
      connected.value = true
      log('WS 已连接 userId=', uid.value)
      startPing()
        connected.value = true
        log('WS 已连接 userId=', uid.value)
        startPing()
    }
    ws.onclose = () => {
      connected.value = false
      log('WS 已关闭')
      stopPing()
        connected.value = false
        log('WS 已关闭')
        stopPing()
    }
    ws.onerror = (e) => log('WS error', e)
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data)
      log('收到信令', msg)
      onSignal(msg)
        const msg = JSON.parse(e.data)
        log('收到信令', msg)
        onSignal(msg)
    }
  }
  function requestCall() {
}
function requestCall() {
    state.value = 'calling'
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    send('call', peerUid.value, null)
    log('➡️ 发起呼叫 call 给', peerUid.value)
  }
  function acceptCall() {
}
function acceptCall() {
    if (!incomingFrom) return
    acceptedByMe = true
    // 停止来电提示
    stopRingtone()
    stopVibration()
    // 先告诉对方我接听了(后端会把双方置忙)
    send('accept', incomingFrom, null)
    log('✅ 已点击接听,发送 accept 给', incomingFrom)
    state.value = 'calling'
    // 如果 offer 已经提前到达(我们暂存了),立刻 answer
    if (lastOfferSdp) {
      answerOffer(lastOfferSdp, incomingFrom)
      lastOfferSdp = null
        answerOffer(lastOfferSdp, incomingFrom)
        lastOfferSdp = null
    }
  }
  function rejectCall() {
}
function rejectCall() {
    if (!incomingFrom) return
    // 停止来电提示
    stopRingtone()
    stopVibration()
    // 你后端没有 reject,用 busy 表示拒绝/不可接听
    send('busy', incomingFrom, null)
    log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
  }
  async function startOffer() {
    // 返回上一页(uni-app的语音通话列表页面)
    if (window.uni) {
        window.uni.navigateBack()
    } else {
        log('❌ uni对象不存在,无法返回上一页')
    }
}
async function startOffer() {
    if (!pc) createPC()
    // ✅ 不会重复 addTrack
    await ensureLocalTracksAdded()
    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    send('offer', peerUid.value, { sdp: pc.localDescription })
    log('➡️ 已发送 offer 给', peerUid.value)
  }
  async function answerOffer(offerSdp, from) {
}
async function answerOffer(offerSdp, from) {
    if (!offerSdp) {
      log('❌ offer sdp 为空,无法接听')
      return
        log('❌ offer sdp 为空,无法接听')
        return
    }
    if (!pc) createPC()
    await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
    await flushCandidates()
    await ensureLocalTracksAdded()
    const answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)
    send('answer', from, { sdp: pc.localDescription })
    log('⬅️ 已发送 answer 给', from)
    state.value = 'in-call'
    startTimer()
  }
  function hangup() {
}
function hangup() {
    cleanup(true)
  }
  /* ---------------- 清理 ---------------- */
  function cleanup(sendToPeer) {
}
/* ---------------- 清理 ---------------- */
function cleanup(sendToPeer) {
    try {
      if (sendToPeer) send('hangup', peerUid.value, null)
        if (sendToPeer) send('hangup', peerUid.value, null)
    } catch {}
    stopTimer()
    // 停止来电提示
    stopRingtone()
    stopVibration()
    if (pc) {
      pc.getSenders().forEach(s => s.track && s.track.stop())
      pc.close()
        pc.getSenders().forEach(s => s.track && s.track.stop())
        pc.close()
    }
    if (localStream) {
      localStream.getTracks().forEach(t => t.stop())
        localStream.getTracks().forEach(t => t.stop())
    }
    pc = null
    localStream = null
    pendingCandidates.length = 0
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
    // 返回上一页(uni-app的语音通话列表页面)
    if (window.uni) {
        window.uni.navigateBack()
    } else {
        log('❌ uni对象不存在,无法返回上一页')
    }
}
// 定义receiveParameters变量,用于模板中判断是否显示呼叫按钮
const receiveParameters = ref(route.query.contact || null);
onMounted(() => {
    const receiveParameters = route.query.contact
    console.log('接收参数', receiveParameters)
    console.log('收拾收拾',route.query.voiceparams);
    console.log('接收参数', receiveParameters.value)
    // 解析并使用contact参数
    if (receiveParameters.value) {
        try {
            const contact = JSON.parse(decodeURIComponent(receiveParameters.value))
            if (contact.friendId) {
                peerUid.value = contact.friendId
                log('🔗 从contact参数获取peerUid:', contact.friendId)
                log('👤 联系人名称:', contact.friendNickName || '未知联系人')
            }
        } catch (e) {
            log('❌ 解析contact参数失败:', String(e))
        }
    }
    // 解析URL参数
    parseUrlParams()
    // 自动连接WebSocket
    connectWS()
});
// 监听连接状态,当connected变为true且有receiveParameters时自动呼叫
watch(() => connected.value, (newVal) => {
    if (newVal && state.value === 'idle' && receiveParameters.value) {
        log('📞 连接成功且有contact参数,自动发起呼叫');
        requestCall();
    }
});
// 监听连接状态,连接成功后自动呼叫
// watch(() => connected.value, (newVal) => {
//     if (newVal && state.value === 'idle') {
//         requestCall()
//     }
// })
onBeforeUnmount(() => {
    cleanup(false)
    stopPing()
    if (ws) ws.close()
})
</script>
<style scoped lang="scss">
.call-container {
    padding: 20px;
    min-height: 100vh;
    font-family: Arial, sans-serif;
    background-color: #f5f5f5;
    transition: all 0.3s ease;
}
  onBeforeUnmount(() => {
    cleanup(false)
    stopPing()
    if (ws) ws.close()
  })
  </script>
  <style scoped lang="scss">
    .call-container {
        padding: 20px;
        min-height: 100vh;
        font-family: Arial, sans-serif;
    }
/* 来电时的全屏样式 */
.call-container.incoming-call {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 99999;
    background-color: #ffffff;
    padding: 0;
}
    .content-wrapper {
        display: flex;
        flex-direction: column;
        gap: 15px;
        max-width: 600px;
        margin: 0 auto;
    }
.content-wrapper {
    display: flex;
    flex-direction: column;
    gap: 20px;
    max-width: 600px;
    margin: 0 auto;
    height: 100vh;
    justify-content: center;
    align-items: center;
}
    .input-group {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
.input-group {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
    .input-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
.input-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
    .input-label {
        font-weight: bold;
        color: #333;
    }
.input-label {
    font-weight: bold;
    color: #333;
}
    .input-field {
        width: 120px;
        padding: 8px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 14px;
    }
.input-field {
    width: 120px;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
}
    .button-group {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        justify-content: center;
    }
.button-group {
    display: flex;
    gap: 50px;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 80px;
}
    .btn {
        padding: 10px 20px;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        transition: background-color 0.3s;
.btn {
    padding: 15px 30px;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 16px;
    font-weight: bold;
    transition: all 0.3s ease;
    min-width: 120px;
        &:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
    &:disabled {
        opacity: 0.6;
        cursor: not-allowed;
    }
}
    .btn-connect {
        background-color: #4CAF50;
    }
/* 来电时的接听和拒绝按钮样式 */
.btn.incoming-accept, .btn.incoming-reject {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
    .btn-call {
        background-color: #2196F3;
    }
.btn.incoming-accept {
    background-color: #4CAF50;
    transform: scale(1.1);
}
    .btn-accept {
        background-color: #4CAF50;
    }
.btn.incoming-reject {
    background-color: #f44336;
}
    .btn-reject {
        background-color: #f44336;
    }
.btn.incoming-accept:active {
    transform: scale(0.95);
}
    .btn-hangup {
        background-color: #9E9E9E;
    }
.btn.incoming-reject:active {
    transform: scale(0.95);
}
    .status-group {
        display: flex;
        flex-direction: column;
        gap: 5px;
    }
.btn-connect {
    background-color: #4CAF50;
}
    .status-text {
        font-size: 16px;
        color: #333;
    }
.btn-call {
    background-color: #2196F3;
}
    .tip-message {
        margin-top: 15px;
        color: #666;
        text-align: center;
    }
.btn-accept {
    background-color: #4CAF50;
}
    .log-container {
        margin-top: 20px;
        max-width: 600px;
        margin-left: auto;
        margin-right: auto;
    }
.btn-reject {
    background-color: #f44336;
}
    .log-box {
        background-color: white;
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 10px;
    }
.btn-hangup {
    background-color: #9E9E9E;
    padding: 15px 40px;
    min-width: 150px;
}
    .log-title {
        font-weight: bold;
        color: #333;
        margin-bottom: 8px;
    }
.status-group {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: center;
}
    .log-content {
        max-height: 200px;
        overflow: auto;
        background-color: #fafafa;
        padding: 10px;
        border-radius: 4px;
        font-size: 12px;
        line-height: 1.4;
    }
.status-text {
    font-size: 18px;
    color: #333;
    text-align: center;
    margin-bottom: 20px;
}
    .hidden-audio {
        display: none;
/* 来电状态样式 */
.status-text.incoming-status {
    font-size: 24px;
    color: #333;
    font-weight: bold;
    animation: pulse 1.5s infinite;
}
.status-text.calling-status {
    font-size: 20px;
    color: #666;
}
/* 呼吸灯动画 */
@keyframes pulse {
    0% {
        opacity: 1;
        transform: scale(1);
    }
    .contactAvatar {
        width: 172rpx;
        height: 172rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        img{
            width: 172rpx;
            height: 172rpx;
        }
    50% {
        opacity: 0.7;
        transform: scale(1.05);
    }
  </style>
    100% {
        opacity: 1;
        transform: scale(1);
    }
}
.tip-message {
    margin-top: 15px;
    color: #666;
    text-align: center;
}
.log-container {
    margin-top: 20px;
    max-width: 600px;
    margin-left: auto;
    margin-right: auto;
}
.log-box {
    background-color: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 10px;
}
.log-title {
    font-weight: bold;
    color: #333;
    margin-bottom: 8px;
}
.log-content {
    max-height: 200px;
    overflow: auto;
    background-color: #fafafa;
    padding: 10px;
    border-radius: 4px;
    font-size: 12px;
    line-height: 1.4;
}
.hidden-audio {
    display: none;
}
/* 头像样式 */
.contactAvatar {
    width: 180px;
    height: 180px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 40px;
    border-radius: 50%;
    background-color: #f0f0f0;
    overflow: hidden;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
    img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }
}
/* 来电时的头像放大效果 */
.call-container.incoming-call .contactAvatar {
    width: 220px;
    height: 220px;
    margin-bottom: 60px;
}
</style>
uniapps/work-app/src/hooks/useGlobalWS.js
@@ -1,104 +1,167 @@
import {useUserStore} from "@/store/index.js";
import {getEnvObj} from "@/utils/index.js";
// import {enterRoom} from "@/utils/voiceCallByTX/index.js";
import { useUserStore } from "@/store/index.js";
import { ref, computed, watch } from "vue";
import useAppStore from "../store/modules/app/index.js";
let socketTask = null
import websocketService from "@/utils/websocket.js";
export function useGlobalWS() {
    const userStore = useUserStore();
  const appStore = useAppStore();
    const userId = computed(() => userStore?.userInfo?.user_id)
    const access_token = computed(() => userStore?.userInfo?.access_token)
    const {VITE_APP_WS_API_URL} = getEnvObj()
  // 设置默认用户ID为3
  const defaultUserId = '3';
  // WebSocket基础URL
  const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
    // 消息处理
    function messageHandler(payload) {
        switch (payload.biz_code) {
            case 'JOB_ISREFRESH':
        appStore.setJobUpdateKeyAdd()
                break
            case 'DEVICE_ISREFRESH':
        appStore.setDeviceUpdateKeyAdd()
                break
            case 'DOWNLOAD_PROGRESS':
                break
            case 'LOGOUT_USER':
                userStore.setUserInfo(null)
                uni.reLaunch({
                    url: '/pages/login/index'
                })
                break
            case 'VoiceCall':
                // enterRoom(payload, userId.value)
                break
            default:
                break;
        }
    }
  function messageHandler(payload) {
    console.log('🌐 全局WebSocket收到消息111111111111111111111:', payload)
    // 先尝试直接处理消息(适用于mobile-web-view的voiceCallDetail页面的消息格式)
    const t = (payload.type || '').toString()
    const bizCode = payload.biz_code || ''
    // 关闭ws
    function closeWS() {
        socketTask?.close({
            success: () => {
                console.log('ws关闭连接');
            },
        })
    }
    console.log('📋 消息分析:type=' + t + ', biz_code=' + bizCode)
    // 初始化ws
    function initWS() {
        // 关闭,再连接ws
        closeWS()
        if (!access_token.value) return
        const url = VITE_APP_WS_API_URL
            + `?x-auth-token=${encodeURI(access_token?.value)}`
            + `&model_type=3&workspace-id=${userId.value}`
        // 创建连接
        socketTask = uni.connectSocket({
            url: url,
            success: () => {
                console.log('ws连接成功');
            },
            fail: (err) => {
                console.error('ws连接失败:', err);
            }
        });
        // 消息监听
        socketTask.onMessage((result) => {
            messageHandler(JSON.parse(result.data))
        })
    //==================================
    // 监听连接打开
    socketTask.onOpen((res) => {
      console.log('✅ WebSocket连接已建立')
      // reconnectAttempts = 0 // 连接成功后重置重连次数
      // 可以在这里发送心跳或订阅消息
      // startHeartbeat()
    })
    // 监听连接关闭
    socketTask.onClose((res) => {
      console.log(`WebSocket连接关闭,代码: ${res.code}, 原因: ${res.reason}`)
    // 处理语音通话请求
    if (t === 'call') {
      console.log('📞 全局收到来电 call,来自', payload.from)
      // 构建来电参数
      const callParams = {
        peerUid: payload.from,
        from: payload.from,
        type: 'incoming'
      };
      // 根据不同的关闭代码处理
      if (res.code === 1000) { // 正常关闭
        console.log('连接正常关闭')
      } else if (res.code === 1006) { // 异常关闭
        console.log('连接异常关闭,尝试重连...')
      } else if (res.code === 1011) { // 服务器内部错误
        console.log('服务器内部错误(1011),延迟重连...')
      } else {
        console.log('其他原因关闭,尝试重连...')
      // 转义参数以便在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环境)
        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;
        } else {
          console.error('无法跳转到语音通话页面:当前环境不支持导航');
        }
      } catch (error) {
        console.error('跳转到语音通话页面失败:', error);
        // 尝试使用外部跳转作为最后的补救措施
        fallbackToExternalNavigation();
      }
    })
    // 监听错误
    socketTask.onError((err) => {
      console.error('WebSocket发生错误:', err)
    })
    }
      // 外部页面跳转的回调函数
      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
    }
    // 然后处理biz_code格式的消息
    switch (bizCode) {
      case 'JOB_ISREFRESH':
        appStore.setJobUpdateKeyAdd()
        break
      case 'DEVICE_ISREFRESH':
        appStore.setDeviceUpdateKeyAdd()
        break
      case 'DOWNLOAD_PROGRESS':
        break
      case 'LOGOUT_USER':
        userStore.setUserInfo(null)
        uni.reLaunch({
          url: '/pages/login/index'
        })
        break
      case 'VoiceCall':
        // enterRoom(payload, userId.value)
        break
      default:
        // 记录未处理的消息
        console.log('未处理的WebSocket消息:', payload)
        break;
    }
  }
    // watch(access_token, initWS, {immediate: true})
  // 初始化WebSocket连接
  function initWS() {
    try {
      // 使用默认用户ID初始化WebSocket连接
      websocketService.init(defaultUserId, WS_BASE);
      console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId);
      // 设置全局消息处理回调
      websocketService.setOnMessageCallback(messageHandler);
      console.log('✅ 全局WebSocket消息处理器已设置');
    } catch (error) {
      console.error('❌ 全局WebSocket初始化失败:', error);
    }
  }
  // 监听用户信息变化,初始化WebSocket
  watch(
    () => userStore.userInfo,
    (newUserInfo) => {
      console.log('👤 用户信息变化,尝试初始化WebSocket');
      initWS();
    },
    { immediate: true }
  )
}
uniapps/work-app/src/pages/login/index.vue
@@ -45,6 +45,7 @@
import md5 from "js-md5";
import { loginByUsername } from "@/api/user/index.js";
import { useUserStore } from "@/store/index.js";
import websocketService from "@/utils/websocket.js";
import { HOME_PATH, LOGIN_PATH, removeQueryString } from "@/router";
@@ -134,7 +135,59 @@
      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);
    }
    uni.reLaunch({
      url: "/pages/work/index",
    });
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
@@ -21,7 +21,7 @@
  // 构建viewUrl,将contact参数拼接到URL中
  const contactParam = contact ? encodeURIComponent(JSON.stringify(contact)) : '';
  // viewUrl.value = `https://192.168.1.157:5176/mobile-web-view/#/webViewWrapper/voiceCallDetail?params=${encodeURIComponent(userParams)}&contact=${contactParam}`;
  viewUrl.value = getWebViewUrl("/voiceCallDetail", { contact: options.contact });
  viewUrl.value = getWebViewUrl("/voiceCallDetail", { contact: options.contact ,voiceparams: options.voiceparams});
});
function onPostMessage(data) {}
// #ifdef APP-PLUS
uniapps/work-app/src/utils/common/index.js
@@ -90,4 +90,4 @@
export function getAssetsImage (targetUrl) {
  const url = getEnvObj().VITE_APP_ASSETS_URL
  return `${url}${targetUrl}`
}
}
uniapps/work-app/src/utils/websocket.js
New file
@@ -0,0 +1,188 @@
// WebSocket服务管理
class WebSocketService {
  constructor() {
    this.socketTask = null
    this.connected = false
    this.url = ''
    this.userId = ''
    this.onMessageCallback = null
    this.onOpenCallback = null
    this.onCloseCallback = null
    this.onErrorCallback = null
    this.pingTimer = null
  }
  // 初始化WebSocket连接
  init(userId, url) {
    if (!url) {
      console.error('WebSocket初始化失败:缺少url')
      return
    }
    this.userId = userId || ''
    this.url = url + encodeURIComponent(this.userId)
    this.connect()
  }
  // 建立WebSocket连接
  connect() {
    try {
      // 关闭现有连接
      this.close()
      // 使用uni-app的WebSocket API
      this.socketTask = uni.connectSocket({
        url: this.url,
        success: () => {
          console.log('WebSocket连接已请求')
        },
        fail: (error) => {
          console.error('WebSocket连接请求失败:', error)
          if (this.onErrorCallback) {
            this.onErrorCallback(error)
          }
        }
      })
      // 监听连接打开
      this.socketTask.onOpen(() => {
        this.connected = true
        console.log('WebSocket已连接,userId:', this.userId)
        this.startPing()
        if (this.onOpenCallback) {
          this.onOpenCallback()
        }
      })
      // 监听连接关闭
      this.socketTask.onClose(() => {
        this.connected = false
        console.log('WebSocket已关闭')
        this.stopPing()
        if (this.onCloseCallback) {
          this.onCloseCallback()
        }
      })
      // 监听连接错误
      this.socketTask.onError((error) => {
        console.error('WebSocket错误:', error)
        this.connected = false
        this.stopPing()
        if (this.onErrorCallback) {
          this.onErrorCallback(error)
        }
      })
      // 监听消息
      this.socketTask.onMessage((res) => {
        try {
          const message = JSON.parse(res.data)
          console.log('收到WebSocket消息:', message)
          if (this.onMessageCallback) {
            this.onMessageCallback(message)
          }
        } catch (error) {
          console.error('解析WebSocket消息失败:', error)
        }
      })
    } catch (error) {
      console.error('WebSocket连接失败:', error)
      if (this.onErrorCallback) {
        this.onErrorCallback(error)
      }
    }
  }
  // 发送消息
  send(type, to, payload) {
    if (!type || !this.socketTask || !this.connected) {
      console.error('WebSocket发送失败:连接未建立')
      return
    }
    const message = {
      type,
      from: String(this.userId),
      to: String(to),
      payload: payload || null,
      timestamp: Date.now()
    }
    this.socketTask.send({
      data: JSON.stringify(message),
      success: () => {
        console.log('WebSocket消息发送成功:', message)
      },
      fail: (error) => {
        console.error('WebSocket消息发送失败:', error)
      }
    })
  }
  // 开始心跳检测
  startPing() {
    this.stopPing()
    // 每25秒发送一次心跳
    this.pingTimer = setInterval(() => {
      if (this.connected) {
        this.send('ping', 'system')
      }
    }, 25000)
  }
  // 停止心跳检测
  stopPing() {
    if (this.pingTimer) {
      clearInterval(this.pingTimer)
      this.pingTimer = null
    }
  }
  // 关闭连接
  close() {
    if (this.socketTask) {
      this.socketTask.close({
        code: 1000,
        reason: '正常关闭',
        success: () => {
          console.log('WebSocket已关闭')
        },
        fail: (error) => {
          console.error('WebSocket关闭失败:', error)
        }
      })
      this.socketTask = null
    }
    this.stopPing()
    this.connected = false
  }
  // 设置消息回调
  setOnMessageCallback(callback) {
    this.onMessageCallback = callback
  }
  // 设置连接打开回调
  setOnOpenCallback(callback) {
    this.onOpenCallback = callback
  }
  // 设置连接关闭回调
  setOnCloseCallback(callback) {
    this.onCloseCallback = callback
  }
  // 设置错误回调
  setOnErrorCallback(callback) {
    this.onErrorCallback = callback
  }
  // 获取连接状态
  getConnected() {
    return this.connected
  }
}
// 导出单例实例
export default new WebSocketService()
uniapps/work-wx/src/api/index.js
@@ -106,4 +106,4 @@
    url: `/system/sys/SysRegionCode/allTree`,
    method: 'get'
  })
}
}
uniapps/work-wx/src/api/user/index.js
@@ -137,3 +137,11 @@
    data: data,
  });
};
// 验证码
export const registerSendCodeApi = (phone) => {
  return request({
    url: `/system/verificationCode/registerSendCode/${phone}`,
    method: 'get'
  })
}
uniapps/work-wx/src/pages/equipmentManagement/index.vue
@@ -1,19 +1,21 @@
<template>
  <view class="equipmentManagement">
    <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="getEquipmentList"></u-search>
    <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search>
    <view class="content">
      <view class="title">飞行设备</view>
      <view class="card" v-for="(item, index) in equipmentList" :key="index">
        <view class="left">
          <u-image :src="droneSvg" mode="aspectFill" :width="70" :height="90"></u-image>
      <u-list :style="{ height: 'calc(100vh - 160rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50">
        <view class="card" v-for="(item, index) in equipmentList" :key="index">
          <view class="left">
            <u-image :src="droneSvg" mode="aspectFill" :width="70" :height="90"></u-image>
          </view>
          <view class="right">
            <view class="name">设备名称:{{ item.name }}</view>
            <view class="txt">设备类型:{{ item.type }}</view>
            <view class="txt">设备型号:{{ item.type }}</view>
          <view class="txt">设备sn:{{ item.sn }}</view>
          </view>
        </view>
        <view class="right">
          <view class="name">设备名称:{{ item.name }}</view>
          <view class="txt">设备类型:{{ item.type }}</view>
          <view class="txt">设备型号:{{ item.type }}</view>
        <view class="txt">设备sn:{{ item.sn }}</view>
        </view>
      </view>
      </u-list>
    </view>
    <view class="add-btn" @click="addEquipment">
      <u-image :src="addSvg" mode="aspectFill" :width="60" :height="68"></u-image>
@@ -29,17 +31,31 @@
import { myDevicePageInfoApi, aircraftInfoPageInfoOfMyApi } from '@/api/index.js'
const equipmentList = ref([])
const noticeTitle = ref('')
const formParams = ref({
  current: 1,
  size: 10,
  model:'',
  name: '',
})
// 搜索处理
const handleSearch = () => {
  // 搜索时重置为第一页
  formParams.value.current = 1
  formParams.value.name = noticeTitle.value
  getEquipmentList()
}
// 设备列表
const getEquipmentList = async () => {
  try {
    const res = await aircraftInfoPageInfoOfMyApi({
      current: 1,
      size: 99,
      model:'',
      name: '',
    })
    equipmentList.value = res.data.data.records || []
    const res = await aircraftInfoPageInfoOfMyApi(formParams.value)
    const newData = res.data.data.records || []
    // 如果是第一页,直接替换数据;否则追加数据
    if (formParams.value.current === 1) {
      equipmentList.value = newData
    } else {
      equipmentList.value = equipmentList.value.concat(newData)
    }
  } catch (error) {
    console.error('获取设备列表失败:', error)
  }
@@ -61,6 +77,14 @@
    url: '/subPackages/deviceRegistration/add',
  })
}
function scrolltolower() {
  formParams.value.current++
  // 停止不刷新
  if (equipmentList.value.length < formParams.value.size) {
    return
  }
  getEquipmentList()
}
onMounted(() => {
  getEquipmentList()
})
uniapps/work-wx/src/pages/legalPolicy/index.vue
@@ -1,21 +1,23 @@
<template>
  <view class="legalPolicy">
    <!-- <u-navbar title="法律政策" @rightClick="rightClick" :autoBack="true"></u-navbar> -->
    <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="getSysNoticeList"></u-search>
    <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search>
    <view class="tabs-container">
      <u-tabs :list="tabs" @click="tabClick"></u-tabs>
    </view>
    <view class="content">
      <view class="card" v-for="(item, index) in announcementList" :key="index" @click="toRegulationsDetail(item)">
        <view class="left">
          <u-image :src="docSvg" mode="aspectFill" :width="30" :height="36"></u-image>
      <u-list :style="{ height: 'calc(100vh - 160rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50">
        <view class="card" v-for="(item, index) in announcementList" :key="index" @click="toRegulationsDetail(item)">
          <view class="left">
            <u-image :src="docSvg" mode="aspectFill" :width="30" :height="36"></u-image>
          </view>
          <view class="right">
            <view class="r-top">{{ item.title }}</view>
            <view class="r-middle">{{ item.description || '暂无描述' }}</view>
            <view class="r-bottom">{{ item.time || '暂无' }}</view>
          </view>
        </view>
        <view class="right">
          <view class="r-top">{{ item.title }}</view>
          <view class="r-middle">{{ item.description || '暂无描述' }}</view>
          <view class="r-bottom">{{ item.time || '暂无' }}</view>
        </view>
      </view>
      </u-list>
    </view>
  </view>
</template>
@@ -36,30 +38,51 @@
  },
])
const currentTab = ref(2)
const formParams = ref({
  current: 1,
  size: 10,
  noticeType: 2,
  noticeTitle: noticeTitle.value,
})
const tabClick = (val) => {
  if (val.index === 0) {
     currentTab.value = 2
    getSysNoticeList()
     formParams.value.noticeType = 2
  } else {
    currentTab.value = 1
    getSysNoticeList()
  }
    formParams.value.noticeType = 1
  }
  formParams.value.current = 1
  announcementList.value = []
  getSysNoticeList()
}
const announcementList = ref()
// 搜索处理
const handleSearch = () => {
  // 搜索时重置为第一页
  formParams.value.current = 1
  formParams.value.noticeTitle = formParams.value.noticeTitle.trim()
  getSysNoticeList()
}
// 触底加载
function scrolltolower() {
  formParams.value.current++
  // 停止不刷新
  if (formParams.value.current > formParams.value.totalPages) {
    uni.showToast({
      title: '没有更多了',
      icon: 'none',
      duration: 2000,
    })
    return
  }
  getSysNoticeList()
}
// 获取通知公告列表
async function getSysNoticeList() {
  try {
    const res = await sysNoticePageInfoApi({
      current: 1,
      size: 10,
      noticeType: currentTab.value,
      noticeTitle: noticeTitle.value,
    })
    announcementList.value = res.data.data.records.map(item => ({
    const res = await sysNoticePageInfoApi(formParams.value)
    const newData = res.data.data.records.map(item => ({
      ...item,
      title: item.noticeTitle,
      description: item.remark,
@@ -67,6 +90,12 @@
      num: item.num,
      like: item.like,
    }))
    // 如果是第一页,直接替换数据;否则追加数据
    if (formParams.value.current === 1) {
        announcementList.value = newData
    } else {
        announcementList.value = announcementList.value.concat(newData)
    }
  } catch (error) {
    uni.showToast({
      title: error.message,
@@ -81,8 +110,6 @@
    url: '/subPackages/regulationsDetail/details?id=' + item.noticeId+'&type=2',
  })
}
onMounted(() => {
  getSysNoticeList()
uniapps/work-wx/src/pages/login/index.vue
@@ -6,10 +6,10 @@
    <div class="title">低空经济服务一体化平台系统</div>
    <view class="user-password">
      <div class="user-name">
        <input v-model="loginForm.username" placeholder="请输入用户名" />
        <u-input border="none" v-model="loginForm.username" placeholder="请输入用户名"></u-input>
      </div>
      <div class="pass-word">
        <u-input type="password" border="bottom" v-model="loginForm.password" placeholder="请输入密码"></u-input>
        <u-input border="none" type="password" v-model="loginForm.password" placeholder="请输入密码"></u-input>
        <!-- <input v-model="loginForm.password" :type="isShowPassword ? 'text' : 'password'" placeholder="请输入密码"/>
        <image v-if="!isShowPassword" :src="showPasswordSvg" @click="isShowPassword = !isShowPassword" />
        <image v-else :src="showPsdSvg"  @click="isShowPassword = !isShowPassword" /> -->
uniapps/work-wx/src/pages/register/index.vue
@@ -10,7 +10,7 @@
             </view>
           </view>
           <view class="row">
            <view class="label">昵称</view>
            <view class="label"><span class="nike"></span>昵称</view>
             <view class="input">
                <u-input input-align="right" border="bottom" v-model="formParams.nikeName" placeholder="昵称"></u-input>
             </view>
@@ -44,7 +44,7 @@
</template>
<script setup>
import { sendResetCode, registerUser } from '@/api/user/index.js';
import { sendResetCode, registerSendCodeApi, registerUser } from '@/api/user/index.js';
const formParams = ref({
    userName: '',
    nikeName: '',
@@ -115,8 +115,7 @@
    }
    // 这里假设账号就是手机号,实际项目中可能需要验证格式
    sendResetCode(formParams.value.userName).then(res => {
        console.log('4444',res)
  registerSendCodeApi(formParams.value.userName).then(res => {
        if (res.data.code === 200) {
            uni.showToast({
                title: '验证码发送成功',
@@ -190,6 +189,10 @@
                title: '注册成功',
                icon: 'success',
            });
            // 跳转登录页面
            uni.navigateTo({
                url: '/pages/login/index'
            })
        } else {
            uni.showToast({
                title: res.data.msg || '注册失败',
@@ -214,7 +217,7 @@
        left: 0;
        width: 100%;
        height: 100%;
        background-image: url("../../static/images/user/userbg.svg")  no-repeat ;
        background-image: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png")  no-repeat ;
        background-size: 100%;
    }
    .content {
@@ -238,6 +241,9 @@
            font-weight: 400;
            font-size: 15px;
            color: #222324;
            .nike {
                margin-right: 16rpx;
            }
            .required {
                color: #FF4444;
            }
uniapps/work-wx/src/pages/user/index.vue
@@ -1,48 +1,45 @@
<!-- 我的 -->
<template>
  <view class="page-wrap">
<view class="pageUser">
    <view class="userBox">
      <view class="flex items-center pb-30rpx pl-30rpx pr-20rpx">
        <view class="mr-20rpx">
          <u-avatar @click="uploadAvatar" :src="user.avatar || showDefaultHeader" size="70" />
        </view>
        <view class="flex-1">
          <view class="userName">{{user.nickName }}</view>
          <view class="departs">
            <image
              src="@/static/images/user/department.svg"
              alt=""
            />
            <span>{{ user?.sysDept?.deptName }}</span>
          </view>
        </view>
      </view>
      <view class="personalInformation">
        <div class="message">
          <div class="messagebox">个人资料</div>
          <div class="editInfo" @click="handelEdit(1)">
            点击编辑
            <image :src="rightImage" alt="" />
          </div>
        </div>
        <div class="passwordBox">
          <div class="messagebox">修改密码</div>
          <div class="editInfo" @click="handelEdit(2)">
            点击编辑
            <image :src="rightImage" alt="" />
          </div>
        </div>
      </view>
      <view class="goOutBtn">
        <div class="goOutStyle" @click="logOut">退出登录</div>
      </view>
    </view>
</view>
    <view class="pageUser">
      <view class="userBox">
        <view class="flex items-center pb-30rpx pl-30rpx pr-20rpx">
          <view class="mr-20rpx">
            <u-avatar @click="uploadAvatar" :src="user.avatar || showDefaultHeader" size="70" />
          </view>
          <view class="flex-1">
            <view class="userName">{{user.nickName }}</view>
            <view class="departs">
              <image
                src="@/static/images/user/department.png"
                alt=""
              />
              <span>{{ user?.sysDept?.deptName }}</span>
            </view>
          </view>
        </view>
        <view class="personalInformation">
          <div class="message">
            <div class="messagebox">个人资料</div>
            <div class="editInfo" @click="handelEdit(1)">
              点击编辑
              <image :src="rightImage" alt="" />
            </div>
          </div>
          <div class="passwordBox">
            <div class="messagebox">修改密码</div>
            <div class="editInfo" @click="handelEdit(2)">
              点击编辑
              <image :src="rightImage" alt="" />
            </div>
          </div>
        </view>
        <view class="goOutBtn">
          <div class="goOutStyle" @click="logOut">退出登录</div>
        </view>
      </view>
    </view>
  </view>
</template>
@@ -54,7 +51,7 @@
import { onShow } from "@dcloudio/uni-app";
import { getDeviceRegionApi } from "@/api/map.js";
import defaultAvatar  from '/static/images/defaultAvatar.svg'
import rightImage from '@/static/images/user/rightBtn.svg';
import rightImage from '@/static/images/user/rightBtn.png';
import showDefaultHeader from "@/static/images/user/default-header.png";
const { setClipboardData, getClipboardData } = useClipboard();
//
@@ -100,7 +97,7 @@
  height: 100%;
}
.pageUser {
  background-image: url("../../static/images/user/userbg.svg")  no-repeat;
  background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png")  no-repeat ;
  background-size: 100%;
}
.userBox {
@@ -213,13 +210,13 @@
  }
  .message {
    background: url('@/static/images/user/info.svg')
    background: url('@/static/images/user/info.png')
      no-repeat center;
    background-size: 100% 100%;
  }
  .passwordBox {
    background: url('@/static/images/user/password.svg')
    background: url('@/static/images/user/password.png')
      no-repeat center;
    background-size: 100% 100%;
  }
uniapps/work-wx/src/static/images/user/default-header.png

uniapps/work-wx/src/static/images/user/default-header.svg
File was deleted
uniapps/work-wx/src/static/images/user/department.svg
File was deleted
uniapps/work-wx/src/static/images/user/droneLogo.svg
File was deleted
uniapps/work-wx/src/static/images/user/info.svg
File was deleted
uniapps/work-wx/src/static/images/user/password.svg
File was deleted
uniapps/work-wx/src/static/images/user/rightBtn.svg
File was deleted
uniapps/work-wx/src/static/images/user/userbg.svg
File was deleted
uniapps/work-wx/src/subPackages/deviceRegistration/add.vue
@@ -144,19 +144,21 @@
        <u-form-item
                label="所属区域"
                labelWidth="230rpx"
                prop="serialNumber"
                prop="regionCode"
                :borderBottom="true"
                ref="item2"
                @click="isShowRegion = true"
        >
            <view class="select-wrapper">
                <u-input
                        v-model="formParams.serialNumber"
                        border="none"
                        placeholder="请选择"
                        readonly
                ></u-input>
                <view class="select-icon">▼</view>
            </view>
            <u-input
                v-model="formParams.regionText"
                disabled
                disabledColor="#ffffff"
                placeholder="请选择所属区域"
                border="none"
            ></u-input>
            <template #right>
                <up-icon name="arrow-down"></up-icon>
            </template>
        </u-form-item>
        <u-form-item
                label="民用航空器登记证书"
@@ -259,9 +261,16 @@
        />
        <!-- 所属区域 -->
        <u-cascader 
            show="show"
            v-model="value"
            :show="isShowRegion"
            v-model="formParams.regionCode"
            :data="areaData"
            value-key="code"
            label-key="name"
            children-key="children"
            :auto-close="true"
            @confirm="onRegionConfirm"
            @close="isShowRegion = false"
            @cancel="isShowRegion = false"
        ></u-cascader>
    </u-form>
    <u-button @click="submitForm" color="#1D6FE9">提交</u-button>
@@ -293,6 +302,8 @@
    hasInsurance: '', // 是否有保险
    hasInsuranceText: '', // 是否有保险文本
    region: '', // 所属区域
    regionCode: '', // 所属区域
    regionText: '', // 所属区域文本
    caacRegistrationCode: '', // 民用航空器登记证书
    caacRegistrationCodeText: '', // 民用航空器登记证书文本
    deviceImage: '', // 飞行器照片
@@ -341,6 +352,38 @@
const onPurchaseDate = (e) => {
    formParams.value.purchaseDate = dayjs(e.value).format('YYYY-MM-DD')
    showPurchaseDate.value = false
}
const findRegionNamesByCodes = (codes, areaData) => {
  const result = []
  let currentData = areaData
  for (const code of codes) {
    // 转换为字符串比较(因为code可能是字符串类型)
    const codeStr = code.toString()
    const item = currentData.find(item => item.code === codeStr)
    if (!item) {
      console.warn(`未找到code为 ${codeStr} 的行政区划`)
      break
    }
    result.push(item.name)
    currentData = item.children || []
  }
  return result
}
// 所属区域确认
const onRegionConfirm = (e) => {
    console.log(e,'888')
    formParams.value.region = e?.join(',')[2] || '' // 用逗号分隔的区域码
    // 通过arrRegion的数组值为[36,3608,360824] 从areaData里面反查出名称
    const regionNames = findRegionNamesByCodes(e, areaData.value)
    console.log(regionNames, '77777')
    formParams.value.regionText = regionNames.join('-') || '' // 用连字符连接的区域名称
    isShowRegion.value = false
}
// 是否有保险
const actionsHasInsurance = ref([
@@ -455,13 +498,13 @@
// 获取所属单位
const isShowRegion = ref(false)
const areaData = ref([])
function getAreaData() {
    areaDataApi().then(res => {
        if (res.data.code === 200) {
            areaData.value = res.data.data
        }
    })
}
// function getAreaData() {
//     areaDataApi().then(res => {
//         if (res.data.code === 200) {
//             areaData.value = res.data.data
//         }
//     })
// }
// 提交图片
function afterReadImage(event) {
    // 获取上传的文件对象
@@ -543,8 +586,29 @@
        // 这里可以添加额外的失败处理逻辑
    }
}
onMounted(() => {
    getAreaData()
onMounted(async () => {
    // 动态导入xzqhData,确保在使用前已初始化
    const { xzqhData } = await import('@/static/xzqh/index')
    // console.log(xzqhData,'9999')
    // xzqhData.map 循环三层
    // 第一层:省份
    // 第二层:城市
    // 第三层:区县
    areaData.value = xzqhData
    // xzqhData.map(item => ({
    //     label: item.name,
    //     value: item.code,
    //     children: item.children.map(child => ({
    //         label: child.name,
    //         value: child.code,
    //         children: child.children?.map(district => ({
    //             label: district.name,
    //             value: district.code
    //         })) || []
    //     }))
    // }))
    // console.log(areaData.value, '8888')
})
</script>
<style scoped lang="scss">
uniapps/work-wx/src/subPackages/flightApplication/index.vue
@@ -12,23 +12,25 @@
            </view>
        </view>
    </view>
    <u-search :bgColor="'white'" :show-action="true" actionText="搜索" :animation="true"></u-search>
    <u-search v-model="formParams.flightPlanName" :bgColor="'white'" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search>
    <view class="content">
        <view class="sp-title">
            审批记录
        </view>
        <view class="approvalRecord" v-for="(approvalRecord,approvalRecordIndex) in approvalRecords" :key="approvalRecordIndex">
            <view class="name-status">
                <view class="name-image">
                    <u-image :src="spSvg" class="icon" width="16" height="16"></u-image>
                    <view class="name">{{approvalRecord.flightPlanName}}</view>
        <u-list :style="{ height: 'calc(100vh - 240rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50">
            <view class="approvalRecord" v-for="(approvalRecord,approvalRecordIndex) in approvalRecords" :key="approvalRecordIndex">
                <view class="name-status">
                    <view class="name-image">
                        <u-image :src="spSvg" class="icon" width="16" height="16"></u-image>
                        <view class="name">{{approvalRecord.flightPlanName}}</view>
                    </view>
                    <view class="status">{{approvalRecord.planStatus || '无状态'}}</view>
                </view>
                <view class="status">{{approvalRecord.planStatus || '无状态'}}</view>
                <view class="txt">飞行设备:{{approvalRecord.device || '无设备'}}</view>
                <view class="txt">行政区划:{{approvalRecord.area || '无行政区划'}}</view>
                <view class="txt">飞行时间:{{approvalRecord.time || '无飞行时间'}}</view>
            </view>
            <view class="txt">飞行设备:{{approvalRecord.device || '无设备'}}</view>
            <view class="txt">行政区划:{{approvalRecord.area || '无行政区划'}}</view>
            <view class="txt">飞行时间:{{approvalRecord.time || '无飞行时间'}}</view>
        </view>
        </u-list>
    </view>
</view>
</template>
@@ -54,65 +56,23 @@
}
// 审批记录是很多条
const approvalRecords = ref([
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
    {
        flightPlanName: '飞行需求名称',
        planStatus: '申请中',
        device: '大疆无人机',
        area: '吉州区',
        time: '2026/01/07至2026/01/08',
    },
])
const approvalRecords = ref([])
const formParams = ref({
    current: 1,
    size: 10,
    flightPlanName: '',
})
// 申请审批列表
const getApprovalRecords = async () => {
    try {
        const res = await flightPlanPageInfoApi({
            current: 1,
            size: 10,
        })
        approvalRecords.value = res.data.data.records || []
        const res = await flightPlanPageInfoApi(formParams.value)
        const newData = res.data.data.records || []
        // 如果是第一页,直接替换数据;否则追加数据
        if (formParams.value.current === 1) {
            approvalRecords.value = newData
        } else {
            approvalRecords.value = approvalRecords.value.concat(newData)
        }
    } catch (error) {
        uni.showToast({
            title: error.message,
@@ -120,6 +80,22 @@
            duration: 2000,
        })
    }
}
// 搜索处理
const handleSearch = () => {
  // 搜索时重置为第一页
  formParams.value.current = 1
  formParams.value.flightPlanName = formParams.value.flightPlanName.trim()
  getApprovalRecords()
}
// 触底加载
function scrolltolower() {
    formParams.value.current++
    // 停止不刷新
    if (approvalRecords.value.length < formParams.value.size) {
        return
    }
    getApprovalRecords()
}
onMounted(() => {
    getApprovalRecords()
@@ -129,9 +105,9 @@
.flightApplication {
    .header-top {
        width: 100%;
        height: 350rpx;
        background: url('@/static/images/fxsq-bg.png') no-repeat center;
        background-size: 100% 100%;
        height: 280rpx;
        background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/fxsq-bg.png")  no-repeat ;
          background-size: 100%;
        position: fixed;
        top: 0;
        .ht-content {
@@ -175,7 +151,7 @@
        width: 90%;
        left: 50%;
        transform: translateX(-50%);
        top: 340rpx !important;
        top: 350rpx !important;
        height: 68rpx;
        flex: 0;
    }
uniapps/work-wx/src/subPackages/regulationsDetail/details.vue
@@ -177,7 +177,7 @@
        }
    }
    .fj-list {
        margin-top: 40rpx;
        // margin-top: 20rpx;
    }
    .fj-item {
        display: flex;
uniapps/work-wx/src/subPackages/userDetail/infos/index.vue
@@ -252,8 +252,9 @@
        left: 0;
        width: 100%;
        height: 100%;
        background-image: url("../../../static/images/user/userbg.svg")  no-repeat ;
        background-size: 100%;
        background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png")  no-repeat ;
          background-size: 100%;
        z-index: -1;
    }
    .avatarBox {
        width: 228rpx;
uniapps/work-wx/src/subPackages/userDetail/password/index.vue
@@ -34,7 +34,7 @@
</template>
<script setup>
    import userbgSvg from "@/static/images/user/userbg.svg";
    import userBgPng from "@/static/images/user/user-bg.png";
    import {
        useUserStore
    } from "@/store/index.js";
@@ -260,8 +260,8 @@
            left: 0;
            width: 100%;
            height: 100%;
            background-image: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ztzf_app_assets/images/user/bg.png");
            background-size: 100%;
            background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png")  no-repeat ;
              background-size: 100%;
        }
        .detailBox {
@@ -293,7 +293,6 @@
                    font-weight: 400;
                    font-size: 30rpx;
                    color: #222324;
                    white-space: nowrap;
                }
                input.input-item {