吉安感知网项目-前端
shuishen
2026-01-12 5667f2b4fc7555ba678d5aef13b096cd38da64db
feat:数据驾驶舱相关处理
14 files modified
3 files added
1982 ■■■■■ changed files
applications/drone-command/src/api/dataCockpit/index.js 8 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/layerControl.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/map/equipment.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/defaultava.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/mh.png patch | view | raw | blame | history
applications/drone-command/src/page/index/top/index.vue 35 ●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common.scss 2 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common/cockpit.scss 660 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/element-ui.scss 261 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/top.scss 7 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/mapUtils.js 7 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 142 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue 395 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue 20 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue 400 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 35 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/index.vue 10 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/api/dataCockpit/index.js
@@ -44,3 +44,11 @@
        data
    })
}
// 设备统计
export const deviceStatisticsApi = () => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/deviceStatistics',
        method: 'get'
    })
}
applications/drone-command/src/assets/images/dataCockpit/layerControl.png
applications/drone-command/src/assets/images/dataCockpit/map/equipment.png
applications/drone-command/src/assets/images/defaultava.png

applications/drone-command/src/assets/images/mh.png

applications/drone-command/src/page/index/top/index.vue
@@ -6,23 +6,16 @@
    </div>
    <div class="top-bar__right">
      <div class="icon-box">
        <top-lock v-if="setting.lock" />
        <top-qna />
        <top-full v-if="setting.fullscreen" title="全屏" />
        <img class="gateway" @click="jumpMH" src="@/assets/images/mh.svg" alt="进入门户" title="进入门户" width="20"
          height="20">
      </div>
      <div class="top-user">
        <img class="top-bar__img" :src="userInfo.avatar" alt="" />
        <div class="icon-box">
         <img class="gateway" @click="jumpMH" src="@/assets/images/mh.png" alt="进入门户" title="进入门户">
        </div>
        <el-dropdown>
          <span class="el-dropdown-link">
            {{ userInfo.real_name }}
            <el-icon class="el-icon--right">
              <arrow-down />
            </el-icon>
            <img class="top-bar__img" :src="userInfo.avatar" alt="" />
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <!--              <el-dropdown-item>
@@ -154,20 +147,25 @@
}
.top-bar__right {
  margin-right: 28px;
  padding-top: 16px;
  flex: 0 0 auto;
  display: flex !important;
  align-items: center;
  align-items: flex-start;
  height: 100%;
  pointer-events: auto;
  box-sizing: border-box;
  .icon-box {
    margin-top: 6px;
    margin-right: 30px;
    display: flex;
    align-items: center;
    gap: 0 20px;
    .gateway {
      width: 25px;
      height: 25px;
      width: 21px;
      height: 18px;
    }
    >* {
@@ -187,10 +185,9 @@
}
.top-bar__img {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-right: 10px;
  margin-top: 5px;
  width: 16px;
  height: 21px;
}
.el-dropdown-link {
applications/drone-command/src/styles/common.scss
@@ -8,6 +8,8 @@
// 地图编辑样式
@import './mapEdit.scss';
@import './common/cockpit.scss';
a {
  text-decoration: none;
  color: #333;
applications/drone-command/src/styles/common/cockpit.scss
New file
@@ -0,0 +1,660 @@
// 数据驾驶舱专属
.ztzf-data-cockpit-search-input {
    .el-input__wrapper {
        padding: 0 8px !important;
        background: #161B2C;
        box-shadow: none !important;
        border: none;
        .el-input__inner {
            color: #ffffff;
            &::placeholder {
                color: #86909C;
            }
        }
    }
    &.is-disabled {
        .el-input__wrapper {
            background-color: #161B2C;
        }
    }
}
.ztzf-data-cockpit-date-picker {
    box-shadow: none !important;
    background: #161B2C;
    border: none;
    border-radius: 5px;
    box-sizing: border-box !important;
    .el-input__wrapper {
        box-shadow: none !important;
        background-color: transparent !important;
        &.is-focus {
            box-shadow: none !important;
        }
    }
    .el-range-input {
        color: #fff;
        &::placeholder {
            color: #86909C;
        }
    }
    .el-range-separator {
        color: #ffffff;
    }
}
.ztzf-data-cockpit-date-picker-popper {
    background: #161B2C !important;
    border: 1px solid !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 {
        .el-icon {
            color: #fff !important;
        }
    }
    .el-date-picker__header-label {
        color: #fff !important;
    }
    .el-picker-panel {
        .el-picker-panel__body-wrapper {
            .el-picker-panel__body {
                background: #161B2C !important;
                color: #fff !important;
                .el-date-picker__time-header {
                    border: none;
                    .el-date-picker__editor-wrap {
                        .el-time-panel {
                            background: #161B2C !important;
                            border-radius: 0px 0px 8px 8px;
                            border: 1px solid !important;
                            border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
                            // 将 time-picker-popper 的样式移到这里
                            .el-time-panel__header {
                                background: #161B2C !important;
                                color: #ffffff !important;
                                border-bottom: 1px solid rgba(71, 157, 255, 0.3) !important;
                            }
                            .el-time-panel__content {
                                .el-time-spinner__item {
                                    color: #e6e6e6 !important;
                                    &:hover {
                                        background: rgba(71, 157, 255, 0.3) !important;
                                    }
                                    &.active {
                                        color: #479dff !important;
                                        font-weight: bold;
                                    }
                                }
                            }
                            .el-time-panel__footer {
                                background: #012350 !important;
                                border-top: 1px solid rgba(71, 157, 255, 0.3) !important;
                                .el-button {
                                    color: #fff !important;
                                    &:hover {
                                        color: #479dff !important;
                                    }
                                }
                                .el-time-panel__btn.cancel {
                                    color: #fff !important;
                                    &:hover {
                                        color: #fff !important;
                                    }
                                }
                            }
                        }
                    }
                }
                .el-picker-panel__content {
                    .el-date-range-picker__header {
                        .el-picker-panel__icon-btn {
                            color: #fff !important;
                            &:hover {
                                color: var(--el-color-primary) !important;
                            }
                            .el-icon {
                                &::before {
                                    color: inherit !important;
                                }
                            }
                        }
                        &>div {
                            span {
                                color: #fff !important;
                            }
                        }
                    }
                    .el-date-table {
                        th {
                            color: #fff !important;
                        }
                        td.disabled div {
                            background-color: #00193b !important;
                            color: #505050 !important;
                            cursor: not-allowed;
                        }
                        // td:hover,
                        // td.current:not(.disabled) {
                        //     background: rgba(60, 121, 202) !important;
                        //     color: white !important;
                        // }
                        td.start-date span,
                        td.start-date span,
                        td.end-date span {
                            background-color: #012350 !important;
                        }
                        td.in-range div,
                        td.in-range div:hover,
                        &.is-week-mode .el-date-table__row.current div,
                        &.is-week-mode .el-date-table__row:hover div {
                            background-color: rgba(60, 121, 202) !important;
                        }
                    }
                }
            }
        }
    }
    .el-popper__arrow {
        &::before {
            background: #161B2C !important;
            border: 1px solid #161B2C !important;
            box-sizing: border-box;
        }
    }
    .el-picker-panel__sidebar {
        background: #012350;
        color: #fff;
        .el-picker-panel__shortcut {
            color: #ffffff;
        }
    }
    .el-picker-panel__body {
        .el-input.is-disabled .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
        .el-button.is-disabled,
        .el-button.is-disabled:hover {
            background: #012350;
        }
        .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
    }
    .el-picker-panel__footer {
        background: #012350;
        color: #fff;
        border-top: none;
        .el-button--small {
            box-shadow: 0 0 0 0.1rem#409eff;
            background-color: #012350;
            color: #fff;
            border: none;
            &:hover {
                background-color: #012350;
                color: #fff;
                border-color: #409eff;
            }
        }
    }
}
.ztzf-data-cockpit-select {
    .el-select__input {
        color: #8ac3fd !important;
    }
    .el-select__wrapper {
        background: #2E2E48;
        box-shadow: none !important;
        border: none;
        width: 100%;
    }
    .el-select__placeholder {
        &.is-transparent {
            color: #86909C !important;
        }
        &.el-select__selected-item {
            color: #ffffff;
        }
    }
    .el-select {
        --el-select-border-color-hover: rgb(0, 162, 255) !important;
    }
    .el-input {
        --el-border-color: rgb(0, 162, 255);
    }
    .el-select .el-input__wrapper {
        background-color: #012350 !important;
    }
    .el-input__wrapper {
        --el-input-text-color: #012350 !important;
    }
}
.ztzf-data-cockpit-select-popper.el-select__popper {
    border: none;
    background: #2E2E48 !important;
    .el-select-dropdown {
        overflow-x: hidden !important;
        border: none;
        .el-scrollbar {
            .el-select-dropdown__wrap {
                overflow-x: hidden !important;
                border: none;
                .el-select-dropdown__list {
                    padding: 0 !important;
                    .el-select-dropdown__item {
                        background: transparent !important;
                        color: white;
                        &.hover {
                            background-color: rgba(0, 120, 233, 0.63) !important;
                            color: white;
                        }
                        &.selected {
                            color: #8ac3fd !important;
                            font-weight: 700;
                        }
                    }
                }
            }
        }
        .el-select-dropdown__empty {
            background: #2E2E48;
            border-radius: 0px 0px 8px 8px;
        }
    }
    .el-popper__arrow::before {
        background: #2E2E48 !important;
        border: 1px solid #2E2E48 !important;
    }
}
// 树形选择
.ztzf-data-cockpit-tree-select-popper {
    background: #2E2E48 !important;
    border: none !important;
    .el-popper__arrow::before {
        background: #2E2E48 !important;
        border: 1px solid #2E2E48 !important;
    }
    .el-tree {
        background: transparent !important;
        color: #fff !important;
        .el-tree-node__content:hover {
            background: none !important;
        }
        .el-tree-node__content:hover {
            background: none !important;
        }
        .el-tree-node.is-current>.el-tree-node__content {
            color: #479dff !important;
        }
    }
    .el-select-dropdown__item {
        background: none !important;
        color: #fff !important;
        input {
            color: #fff !important;
            &::placeholder {
                color: #fff !important;
            }
        }
    }
    .el-tree {
        --el-tree-node-hover-bg-color: none !important;
        color: #fff !important;
        .el-checkbox__inner {
            background: none !important;
            border: 1px solid #3194ef !important;
        }
    }
    .el-select-dropdown__item.is-selected {
        color: #fff !important;
    }
    .el-popper.is-light {
        background: #012a50 !important;
        box-shadow: none !important;
        border: none !important;
        border-radius: 4px !important;
        margin-top: 2px;
        .el-select__selection {
            max-width: 240px !important;
            .el-select__selected-item {
                .el-tag {
                    background: #0b1d38 !important;
                    color: white;
                }
            }
        }
        .el-popper__arrow::before {
            background: #012a50 !important;
            border: 1px solid #012a50 !important;
        }
    }
}
.ztzf-data-cockpit-dialog {
    padding: 20px;
    display: flex;
    flex-direction: column;
    width: 1222px;
    height: 835px;
    background: #1A1A2A;
    border-radius: 6px 6px 6px 6px;
    border: 1px solid #2E2E46;
    box-sizing: border-box;
    .el-dialog__header {
        padding-bottom: 30px;
        height: auto;
        .el-dialog__title {
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: bold;
            font-size: 18px;
            color: #FFFFFF;
            line-height: 22px;
            text-align: left;
            font-style: normal;
            text-transform: none;
        }
        .el-dialog__headerbtn {
            .el-icon.el-dialog__close {
                color: #fff;
            }
        }
    }
    .history-search {
        .el-form-item__label {
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: 500;
            font-size: 14px;
            color: #C5C9D6;
            font-style: normal;
            text-transform: none;
        }
        .ztzf-data-cockpit-search-input {
            .el-input__wrapper {
                background: #2E2E48;
            }
            &.is-disabled {
                .el-input__wrapper {
                    background-color: #2E2E48;
                }
            }
        }
        .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;
                }
            }
        }
        .history-search-actions {
            .el-button {
                width: 96px;
                background: transparent !important;
                border-radius: 4px 4px 4px 4px;
                border: 1px solid rgba(255, 255, 255, 0.3);
                .el-icon {
                    font-size: 16px;
                    color: #fff;
                }
            }
        }
    }
    .el-dialog__body {
        height: 0;
        flex: 1;
        display: flex;
        flex-direction: column;
        .table-container {
            height: 0;
            flex: 1;
            display: flex;
            flex-direction: column;
            .table-content {
                height: 0;
                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 {
                        border-bottom: 1px solid #323241;
                        background: transparent !important;
                        .cell {
                            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 {
                        border-bottom: 1px solid #323241;
                        background: transparent !important;
                        .cell {
                            font-family: Source Han Sans CN, Source Han Sans CN;
                            font-weight: 400;
                            font-size: 14px;
                            color: #9E9EBA;
                            line-height: 18px;
                            font-style: normal;
                            text-transform: none;
                        }
                    }
                }
            }
            .table-pagination {
                padding-top: 30px;
                padding-bottom: 10px;
                display: flex;
                justify-content: flex-end;
                .el-pagination__total {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 14px;
                    color: #9E9EBA;
                    font-style: normal;
                    text-transform: none;
                }
                .btn-prev,
                .btn-next {
                    background: transparent !important;
                    .el-icon {
                        color: #C9CDD4;
                    }
                }
                .el-pager {
                    li {
                        margin-left: 8px;
                        font-family: Nunito Sans, Nunito Sans;
                        font-weight: 600;
                        font-size: 14px;
                        color: #9E9EBA;
                        font-style: normal;
                        text-transform: none;
                        background: transparent;
                        &:first-child {
                            margin-left: 0;
                        }
                        &.is-active {
                            font-family: Nunito Sans, Nunito Sans;
                            font-weight: 600;
                            font-size: 14px;
                            color: #477EFF;
                            font-style: normal;
                            text-transform: none;
                            background: rgba(71, 126, 255, 0.16);
                        }
                    }
                }
                .el-select {
                    .el-select__wrapper {
                        .el-select__placeholder {
                            font-family: Source Han Sans CN, Source Han Sans CN;
                            font-weight: 400;
                            font-size: 14px;
                            color: #9E9EBA;
                            font-style: normal;
                            text-transform: none;
                        }
                        background: #1C1C31;
                        border: none;
                        box-shadow: none;
                    }
                }
            }
        }
    }
}
applications/drone-command/src/styles/element-ui.scss
@@ -766,265 +766,4 @@
        box-sizing: border-box;
        background: #fafafa;
    }
}
.ztzf-data-cockpit-search-input {
    .el-input__wrapper {
        padding: 0 8px !important;
        background: #161B2C;
        box-shadow: none !important;
        border: none;
        .el-input__inner {
            color: #ffffff;
            &::placeholder {
                color: #86909C;
            }
        }
    }
    &.is-disabled {
        .el-input__wrapper {
            background-color: #161B2C;
        }
    }
}
.ztzf-data-cockpit-date-picker {
    box-shadow: none !important;
    background: #161B2C;
    border: none;
    border-radius: 5px;
    box-sizing: border-box !important;
    .el-input__wrapper {
        box-shadow: none !important;
        background-color: transparent !important;
        &.is-focus {
            box-shadow: none !important;
        }
    }
    .el-range-input {
        color: #fff;
        &::placeholder {
            color: #86909C;
        }
    }
    .el-range-separator {
        color: #ffffff;
    }
}
.ztzf-data-cockpit-date-picker-popper {
    background: #161B2C !important;
    border: 1px solid !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 {
        .el-icon {
            color: #fff !important;
        }
    }
    .el-date-picker__header-label {
        color: #fff !important;
    }
    .el-picker-panel {
        .el-picker-panel__body-wrapper {
            .el-picker-panel__body {
                background: #161B2C !important;
                color: #fff !important;
                .el-date-picker__time-header {
                    border: none;
                    .el-date-picker__editor-wrap {
                        .el-time-panel {
                            background: #161B2C !important;
                            border-radius: 0px 0px 8px 8px;
                            border: 1px solid !important;
                            border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
                            // 将 time-picker-popper 的样式移到这里
                            .el-time-panel__header {
                                background: #161B2C !important;
                                color: #ffffff !important;
                                border-bottom: 1px solid rgba(71, 157, 255, 0.3) !important;
                            }
                            .el-time-panel__content {
                                .el-time-spinner__item {
                                    color: #e6e6e6 !important;
                                    &:hover {
                                        background: rgba(71, 157, 255, 0.3) !important;
                                    }
                                    &.active {
                                        color: #479dff !important;
                                        font-weight: bold;
                                    }
                                }
                            }
                            .el-time-panel__footer {
                                background: #012350 !important;
                                border-top: 1px solid rgba(71, 157, 255, 0.3) !important;
                                .el-button {
                                    color: #fff !important;
                                    &:hover {
                                        color: #479dff !important;
                                    }
                                }
                                .el-time-panel__btn.cancel {
                                    color: #fff !important;
                                    &:hover {
                                        color: #fff !important;
                                    }
                                }
                            }
                        }
                    }
                }
                .el-picker-panel__content {
                    .el-date-range-picker__header {
                        .el-picker-panel__icon-btn {
                            color: #fff !important;
                            &:hover {
                                color: var(--el-color-primary) !important;
                            }
                            .el-icon {
                                &::before {
                                    color: inherit !important;
                                }
                            }
                        }
                        &>div {
                            span {
                                color: #fff !important;
                            }
                        }
                    }
                    .el-date-table {
                        th {
                            color: #fff !important;
                        }
                        td.disabled div {
                            background-color: #00193b !important;
                            color: #505050 !important;
                            cursor: not-allowed;
                        }
                        // td:hover,
                        // td.current:not(.disabled) {
                        //     background: rgba(60, 121, 202) !important;
                        //     color: white !important;
                        // }
                        td.start-date span,
                        td.start-date span,
                        td.end-date span {
                            background-color: #012350 !important;
                        }
                        td.in-range div,
                        td.in-range div:hover,
                        &.is-week-mode .el-date-table__row.current div,
                        &.is-week-mode .el-date-table__row:hover div {
                            background-color: rgba(60, 121, 202) !important;
                        }
                    }
                }
            }
        }
    }
    .el-popper__arrow {
        &::before {
            background: #161B2C !important;
            border: 1px solid #161B2C !important;
            box-sizing: border-box;
        }
    }
    .el-picker-panel__sidebar {
        background: #012350;
        color: #fff;
        .el-picker-panel__shortcut {
            color: #ffffff;
        }
    }
    .el-picker-panel__body {
        .el-input.is-disabled .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
        .el-button.is-disabled,
        .el-button.is-disabled:hover {
            background: #012350;
        }
        .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
    }
    .el-picker-panel__footer {
        background: #012350;
        color: #fff;
        border-top: none;
        .el-button--small {
            box-shadow: 0 0 0 0.1rem#409eff;
            background-color: #012350;
            color: #fff;
            border: none;
            &:hover {
                background-color: #012350;
                color: #fff;
                border-color: #409eff;
            }
        }
    }
}
.ztzf-data-cockpit-dialog {
    width: 1222px;
    height: 835px;
    background: #1A1A2A;
    border-radius: 6px 6px 6px 6px;
    border: 1px solid #2E2E46;
}
applications/drone-command/src/styles/top.scss
@@ -56,13 +56,6 @@
}
.top-bar__img {
  margin: 0 5px;
  padding: 2px;
  width: 30px;
  height: 30px;
  border-radius: 100%;
  box-sizing: border-box;
  border: 1px solid #eee;
  vertical-align: middle;
}
applications/drone-command/src/utils/mapUtils.js
@@ -315,6 +315,13 @@
    // 销毁处理
    destroy () {
        if (!this._viewer || this._viewer.isDestroyed?.()) {
            this._primitiveCollection = null
            this._entityCircleDataSource = null
            this._viewer = null
            return
        }
        if (this._primitiveCollection) {
            this._primitiveCollection.removeAll()
            this._viewer.scene.primitives.remove(this._primitiveCollection)
applications/drone-command/src/views/api.txt
@@ -1,48 +1,22 @@
## 信号干扰,诱导驱离
## 历史告警-实时告警
**接口地址**:`/cockpit/dataCockpit/interferenceAndExpulsion`
**接口地址**:`/cockpit/dataCockpit/alarmLog`
**请求方式**:`POST`
**请求方式**:`GET`
**请求数据类型**:`application/json`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>传入fwEffectEval</p>
**接口描述**:<p>传入fwDroneAlarmRecord</p>
**请求示例**:
```javascript
{
  "alarmRecordId": 0,
  "areaCode": "",
  "counterEffect": "",
  "coverRadiusM": 0,
  "deployLatitude": 0,
  "deployLongitude": 0,
  "deviceCode": "",
  "deviceId": 0,
  "deviceModel": "",
  "deviceName": "",
  "deviceType": "",
  "droneDeviceCode": "",
  "droneName": "",
  "droneType": "",
  "findTime": "",
  "id": 0,
  "workMode": ""
}
```
**请求参数**:
@@ -53,24 +27,31 @@
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|fwEffectEval|fwEffectEval|body|true|FwEffectEvalDTO|FwEffectEvalDTO|
|&emsp;&emsp;alarmRecordId|告警记录ID(ja_fw_drone_alarm_record.id)||false|integer(int64)||
|&emsp;&emsp;areaCode|区域编码||false|string||
|&emsp;&emsp;counterEffect|反制效果(success/fail)||false|string||
|&emsp;&emsp;coverRadiusM|覆盖范围(米)||false|integer(int32)||
|&emsp;&emsp;deployLatitude|部署纬度(冗余)||false|number(double)||
|&emsp;&emsp;deployLongitude|部署经度(冗余)||false|number(double)||
|&emsp;&emsp;deviceCode|反制设备编码(快照)||false|string||
|&emsp;&emsp;deviceId|反制设备ID(ja_fw_device.id)||false|integer(int64)||
|&emsp;&emsp;deviceModel|反制设备型号(快照)||false|string||
|&emsp;&emsp;deviceName|反制设备名称(快照)||false|string||
|&emsp;&emsp;deviceType|反制设备类型(快照)||false|string||
|&emsp;&emsp;droneDeviceCode|无人机设备编码||false|string||
|&emsp;&emsp;droneName|无人机名称||false|string||
|&emsp;&emsp;droneType|无人机类型||false|string||
|&emsp;&emsp;findTime|发现时间||false|string(date-time)||
|&emsp;&emsp;id|主键id||false|integer(int64)||
|&emsp;&emsp;workMode|工作模式(机动/固定)||false|string||
|alarmTime|告警时间|query|false|string(date-time)||
|alarmType|告警类型:1.实时告警/2.历史告警|query|false|string||
|areaCode|区域编码|query|false|string||
|areaDivideId|区域id ja_fw_area_divide|query|false|string||
|areaName|区域名称|query|false|string||
|counterWay|反制方式:1.信号干扰/2.诱导驱离/无|query|false|string||
|current|当前页|query|false|integer(int32)||
|defenseSceneId|场景id ja_fw_defense_scene.id|query|false|string||
|deviceId|反无设备ID(关联ja_fw_device.id)|query|false|integer(int64)||
|deviceType|设备类型|query|false|string||
|droneName|无人机名称|query|false|string||
|droneSerialNo|无人机序列号|query|false|string||
|droneType|无人机类型(1.微型机/2.植保机...)|query|false|string||
|endTime|结束时间|query|false|string||
|flightHeightM|飞行高度(m)|query|false|integer(int32)||
|flightSpeedMs|飞行速度(m/s)|query|false|number||
|flightStatus|飞行状态:1.侦测中/2.反制中|query|false|string||
|id|主键id|query|false|integer(int64)||
|latitude|当前纬度|query|false|number(double)||
|longitude|当前经度|query|false|number(double)||
|signalFreqMhz|信号频段(MHz)|query|false|integer(int32)||
|size|每页的数量|query|false|integer(int32)||
|startTime|开始时间|query|false|string||
|stayDuration|停留时长 (秒) |query|false|string||
|triggerReason|触发原因|query|false|string||
**响应状态**:
@@ -78,8 +59,7 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- | 
|200|OK|R|
|201|Created||
|200|OK|R«IPage«FwDroneAlarmRecordVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -91,7 +71,33 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- | 
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|object||
|data|承载数据|IPage«FwDroneAlarmRecordVO»|IPage«FwDroneAlarmRecordVO»|
|&emsp;&emsp;current||integer(int64)||
|&emsp;&emsp;pages||integer(int64)||
|&emsp;&emsp;records||array|FwDroneAlarmRecordVO|
|&emsp;&emsp;&emsp;&emsp;alarmTime|告警时间|string||
|&emsp;&emsp;&emsp;&emsp;alarmType|告警类型:1.实时告警/2.历史告警|string||
|&emsp;&emsp;&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;&emsp;&emsp;areaName|区域名称|string||
|&emsp;&emsp;&emsp;&emsp;counterWay|反制方式:1.信号干扰/2.诱导驱离/3.无|string||
|&emsp;&emsp;&emsp;&emsp;deviceId|反无设备ID(关联ja_fw_device.id)|integer||
|&emsp;&emsp;&emsp;&emsp;deviceModel|设备型号|string||
|&emsp;&emsp;&emsp;&emsp;deviceName|设备名称|string||
|&emsp;&emsp;&emsp;&emsp;deviceType|设备类型(1.便捷侦测箱/2.反制枪/3.察打一体)|string||
|&emsp;&emsp;&emsp;&emsp;droneName|无人机名称|string||
|&emsp;&emsp;&emsp;&emsp;droneSerialNo|无人机序列号|string||
|&emsp;&emsp;&emsp;&emsp;droneType|无人机类型(1.微型机/2.植保机...)|string||
|&emsp;&emsp;&emsp;&emsp;flightHeightM|飞行高度(m)|integer||
|&emsp;&emsp;&emsp;&emsp;flightSpeedMs|飞行速度(m/s)|number||
|&emsp;&emsp;&emsp;&emsp;flightStatus|飞行状态:1.侦测中/2.反制中|string||
|&emsp;&emsp;&emsp;&emsp;id|主键id|integer||
|&emsp;&emsp;&emsp;&emsp;latitude|当前纬度|number||
|&emsp;&emsp;&emsp;&emsp;longitude|当前经度|number||
|&emsp;&emsp;&emsp;&emsp;signalFreqMhz|信号频段(MHz)|integer||
|&emsp;&emsp;&emsp;&emsp;stayDuration|停留时长(秒)|string||
|&emsp;&emsp;&emsp;&emsp;triggerReason|触发原因|string||
|&emsp;&emsp;size||integer(int64)||
|&emsp;&emsp;total||integer(int64)||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -100,7 +106,37 @@
```javascript
{
    "code": 0,
    "data": {},
    "data": {
        "current": 0,
        "pages": 0,
        "records": [
            {
                "alarmTime": "",
                "alarmType": "",
                "areaCode": "",
                "areaName": "",
                "counterWay": "",
                "deviceId": 0,
                "deviceModel": "",
                "deviceName": "",
                "deviceType": "",
                "droneName": "",
                "droneSerialNo": "",
                "droneType": "",
                "flightHeightM": 0,
                "flightSpeedMs": 0,
                "flightStatus": "",
                "id": 0,
                "latitude": 0,
                "longitude": 0,
                "signalFreqMhz": 0,
                "stayDuration": "",
                "triggerReason": ""
            }
        ],
        "size": 0,
        "total": 0
    },
    "msg": "",
    "success": true
}
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue
@@ -1,94 +1,87 @@
<template>
    <el-dialog
        class="ztzf-data-cockpit-dialog"
        append-to-body
        v-model="visibleModel"
        :title="dialogTitle"
        :close-on-click-modal="false"
        :destroy-on-close="true"
    >
        <div class="dialog-body">
            <el-form ref="queryParamsRef" :model="searchParams" class="history-search">
                <el-row :gutter="12">
                    <el-col :span="5">
                        <el-form-item label="关键字" prop="keyword">
                            <el-input v-model="searchParams.keyword" placeholder="名称/序列号" clearable @clear="handleSearch" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="5">
                        <el-form-item label="日期" prop="dateRange">
                            <el-date-picker
                                v-model="searchParams.dateRange"
                                type="daterange"
                                range-separator="至"
                                start-placeholder="开始日期"
                                end-placeholder="结束日期"
                                value-format="YYYY/MM/DD"
                                @change="handleSearch"
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="5">
                        <el-form-item label="区域" prop="area">
                            <el-tree-select
                                v-model="searchParams.area"
                                :data="areaTree"
                                :props="areaTreeProps"
                                node-key="value"
                                check-strictly
                                clearable
                                placeholder="请选择"
                                @change="handleSearch"
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="5">
                        <el-form-item label="无人机类型" prop="droneType">
                            <el-select
                                v-model="searchParams.droneType"
                                placeholder="请选择"
                                clearable
                                @change="handleSearch"
                            >
                                <el-option v-for="item in droneTypeOptions" :key="item" :label="item" :value="item" />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="4" class="history-search-actions">
                        <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>
    <el-dialog class="ztzf-data-cockpit-dialog" append-to-body v-model="visibleModel" :title="dialogTitle"
        :close-on-click-modal="false" :destroy-on-close="true">
        <el-form ref="queryParamsRef" :model="searchParams" class="history-search">
            <el-row :gutter="12">
                <el-col :span="4">
                    <el-form-item label="关键字" prop="keyword">
                        <el-input class="ztzf-data-cockpit-search-input" v-model="searchParams.keyword"
                            placeholder="名称/序列号" clearable @clear="handleSearch" />
                    </el-form-item>
                </el-col>
                <el-col :span="5">
                    <el-form-item prop="dateRange">
                        <el-date-picker class="ztzf-data-cockpit-date-picker"
                            popper-class="ztzf-data-cockpit-date-picker-popper" v-model="searchParams.dateRange"
                            type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
                            value-format="YYYY/MM/DD" @change="handleSearch" />
                    </el-form-item>
                </el-col>
                <el-col :span="4">
                    <el-form-item label="区域" prop="area">
                        <el-tree-select  class="ztzf-data-cockpit-select" popper-class="ztzf-tree-cockpit-select-popper" v-model="searchParams.area" :data="areaTree" :props="areaTreeProps"
                            node-key="value" check-strictly clearable placeholder="请选择" @change="handleSearch" />
                    </el-form-item>
                </el-col>
                <el-col :span="5">
                    <el-form-item label="无人机类型" prop="droneType">
                        <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper"
                            v-model="searchParams.droneType" placeholder="请选择" clearable @change="handleSearch">
                            <el-option v-for="item in droneTypeOptions" :key="item.value" :label="item.label"
                                :value="item.value" />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="6" class="history-search-actions">
                    <el-form-item>
                        <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                        <el-button :icon="Search" @click="handleSearch"></el-button>
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
            <el-table v-loading="loading" :data="pagedList" height="360" class="history-table">
                <el-table-column type="index" width="60" label="序号" />
                <el-table-column prop="droneName" label="无人机名称" min-width="120" />
                <el-table-column prop="serialNumber" label="无人机序列号" min-width="140" />
                <el-table-column prop="droneType" label="无人机类型" min-width="110" />
                <el-table-column prop="area" label="区域" min-width="90" />
                <el-table-column prop="dataSource" label="数据来源" min-width="110" />
                <el-table-column prop="frequency" label="信号频段" min-width="100" />
                <el-table-column prop="discoveredAt" label="发现时间" min-width="150" />
                <el-table-column prop="stayTime" label="停留时间" min-width="110" />
                <el-table-column prop="counterMethod" label="反制方式" min-width="110" />
            </el-table>
            <div class="history-pagination">
                <el-pagination
                    v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size"
                    :total="total"
                />
        <div class="table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
            <div class="table-content">
                <el-table class="ztzf-data-cockpit-table" :data="list">
                    <el-table-column type="index" width="60" label="序号" />
                    <el-table-column prop="droneName" label="无人机名称" min-width="120" />
                    <el-table-column prop="droneSerialNo" label="无人机序列号" min-width="140" />
                    <el-table-column label="无人机类型" min-width="110">
                        <template #default="{ row }">
                            {{ getDroneTypeText(row.droneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="areaName" label="区域" min-width="90" />
                    <el-table-column prop="deviceName" label="数据来源" min-width="110" />
                    <el-table-column label="信号频段" min-width="100">
                        <template #default="{ row }">
                            {{ formatFrequency(row.signalFreqMhz) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="alarmTime" label="发现时间" min-width="150" />
                    <el-table-column prop="stayDuration" label="停留时间" min-width="110" />
                    <el-table-column label="反制方式" min-width="110">
                        <template #default="{ row }">
                            {{ getCounterWayText(row.counterWay) }}
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <div class="table-pagination">
                <el-pagination popper-class="ztzf-data-cockpit-select-popper" v-model:current-page="searchParams.current" v-model:page-size="searchParams.size"
                    layout="total, prev, pager, next, sizes"
                    :total="total" @change="getList" />
            </div>
        </div>
    </el-dialog>
</template>
<script setup>
import { computed, ref } from 'vue'
import { Search, RefreshRight } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
import { alarmLogApi } from '@/api/dataCockpit'
const props = defineProps({
    modelValue: {
@@ -118,11 +111,13 @@
    area: '',
    droneType: '',
    current: 1,
    size: 6
    size: 10
})
const searchParams = ref(initSearchParams())
const queryParamsRef = ref(null)
const loading = ref(false)
const list = ref([])
const total = ref(0)
const areaTree = ref([
    {
        label: '古村区',
@@ -142,167 +137,79 @@
    children: 'children'
}
const historyList = computed(() => {
    const dataSource = props.device?.name || '侦测反制设备名称'
    return [
        {
            id: 'his_001',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '5800MHZ',
            discoveredAt: '2025/12/26 12:15:26',
            stayTime: '00:10:50',
            counterMethod: '信号干扰'
        },
        {
            id: 'his_002',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '5800MHZ',
            discoveredAt: '2025/12/26 12:18:06',
            stayTime: '00:08:12',
            counterMethod: '信号干扰'
        },
        {
            id: 'his_003',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '2400MHZ',
            discoveredAt: '2025/12/26 12:22:31',
            stayTime: '00:06:09',
            counterMethod: '诱导驱离'
        },
        {
            id: 'his_004',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '2400MHZ',
            discoveredAt: '2025/12/26 12:28:44',
            stayTime: '00:12:03',
            counterMethod: '诱导驱离'
        },
        {
            id: 'his_005',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '5800MHZ',
            discoveredAt: '2025/12/26 12:32:10',
            stayTime: '00:04:39',
            counterMethod: '信号干扰'
        },
        {
            id: 'his_006',
            droneName: '无人机名称',
            serialNumber: 'XLH789456',
            droneType: '微型机',
            area: '古村区',
            dataSource,
            frequency: '5800MHZ',
            discoveredAt: '2025/12/26 12:36:42',
            stayTime: '00:07:21',
            counterMethod: '信号干扰'
const droneTypeOptions = [
    { label: '微型机', value: '1' },
    { label: '植保机', value: '2' }
]
const getList = async () => {
    if (!props.device?.id) {
        list.value = []
        total.value = 0
        return
    }
    const [startTime, endTime] = searchParams.value.dateRange || []
    const keyword = (searchParams.value.keyword || '').trim()
    const params = {
        alarmType: '2',
        deviceId: props.device.id,
        current: searchParams.value.current,
        size: searchParams.value.size,
        startTime,
        endTime,
        areaName: searchParams.value.area || undefined,
        droneType: searchParams.value.droneType || undefined,
        droneName: keyword || undefined,
    }
    loading.value = true
    try {
        const res = await alarmLogApi(params)
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
        loading.value = false
    }
}
function handleSearch () {
    searchParams.value.current = 1
    getList()
}
function resetForm () {
    searchParams.value = initSearchParams()
    getList()
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHZ`
}
const getCounterWayText = (value) => {
    if (value === 1 || value === '1') return '信号干扰'
    if (value === 2 || value === '2') return '诱导驱离'
    if (value === 3 || value === '3') return '无'
    return value || '-'
}
const getDroneTypeText = (value) => {
    if (value === 1 || value === '1') return '微型机'
    if (value === 2 || value === '2') return '植保机'
    return value || '-'
}
watch(
    () => visibleModel.value,
    (visible) => {
        if (visible) {
            searchParams.value.current = 1
            getList()
        }
    ]
})
const droneTypeOptions = computed(() => {
    const types = historyList.value.map((item) => item.droneType).filter(Boolean)
    return Array.from(new Set(types))
})
const filteredList = computed(() => {
    const { keyword, dateRange, area, droneType } = searchParams.value
    const [startDate, endDate] = dateRange || []
    return historyList.value.filter((item) => {
        const keywordMatch =
            !keyword || item.droneName.includes(keyword) || item.serialNumber.includes(keyword)
        const typeMatch = !droneType || item.droneType === droneType
        const areaMatch = !area || item.area.includes(area)
        const dateMatch =
            !startDate || !endDate || isWithinDateRange(item.discoveredAt, startDate, endDate)
        return keywordMatch && typeMatch && areaMatch && dateMatch
    })
})
const total = computed(() => filteredList.value.length)
const pagedList = computed(() => {
    const { current, size } = searchParams.value
    const start = (current - 1) * size
    return filteredList.value.slice(start, start + size)
})
function handleSearch() {
    searchParams.value.current = 1
}
function resetForm() {
    queryParamsRef.value?.resetFields()
    searchParams.value.current = 1
}
function isWithinDateRange(value, start, end) {
    const dateValue = new Date(value)
    const startDate = new Date(`${start} 00:00:00`)
    const endDate = new Date(`${end} 23:59:59`)
    return dateValue >= startDate && dateValue <= endDate
}
    }
)
</script>
<style lang="scss" scoped>
.dialog-body {
    display: flex;
    flex-direction: column;
    gap: 16px;
    color: #C3C3DD;
}
.history-search {
    padding: 8px 12px 4px;
    background: rgba(10, 14, 34, 0.6);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 6px;
}
.history-search :deep(.el-date-editor),
.history-search :deep(.el-select),
.history-search :deep(.el-tree-select) {
    width: 100%;
}
.history-search-actions :deep(.el-form-item__content) {
    justify-content: flex-end;
    gap: 8px;
}
.history-table {
    margin-top: 8px;
    --el-table-border-color: rgba(255, 255, 255, 0.08);
    --el-table-header-bg-color: rgba(20, 24, 48, 0.9);
    --el-table-row-hover-bg-color: rgba(255, 255, 255, 0.03);
    --el-table-text-color: #C3C3DD;
    --el-table-header-text-color: #FFFFFF;
}
.history-pagination {
    display: flex;
    justify-content: flex-end;
    padding-top: 8px;
}
</style>
<style lang="scss" scoped></style>
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue
@@ -115,17 +115,12 @@
    position: absolute;
    top: 50%;
    left: 357px;
    background: rgba(17, 23, 34, 0.8);
    border: 1px solid #333355;
    border-left: none;
    border-radius: 0 4px 4px 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 10;
    backdrop-filter: blur(5px);
    transform: translateY(-50%);
    .arrow {
@@ -140,8 +135,19 @@
}
.category-container {
    position: relative;
    display: flex;
    justify-content: space-between;
    &::after {
        content: '';
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        height: 1px;
        background: #333355;
    }
    .category-item {
        position: relative;
@@ -175,8 +181,7 @@
            left: 0;
            bottom: 0;
            width: 100%;
            height: 1px;
            background: #333355;
            height: 0;
        }
        &.active::after {
@@ -184,6 +189,7 @@
            background: #284FE3;
            box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
            border-radius: 5px 5px 0px 0px;
            z-index: 9;
        }
        .badge {
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
@@ -1,28 +1,274 @@
<template>
    <div class="ztzf-cesium map-container" id="cesium">
    <div class="ztzf-cesium map-container" id="cesium"></div>
    <div class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
        <div class="layer-control-wrap" ref="layerWrapRef">
            <div class="layer-control" @click="toggleLayerPanel">
                <img :src="layerControlIcon" alt="图层控制">
            </div>
            <div v-if="showLayerPanel" class="layer-panel">
                <div class="panel-title">图层管理</div>
                <div class="panel-content">
                    <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key" :props="layerTreeProps"
                        :default-checked-keys="defaultCheckedKeys" />
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
import * as Cesium from 'cesium'
import { PublicCesium } from '@/utils/cesium/publicCesium'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
const props = defineProps({
    onlineDevices: {
        type: Array,
        default: () => []
    },
    leftCollapsed: {
        type: Boolean,
        default: false
    }
})
let viewInstance = null
let viewer = null
const deviceEntityIds = new Set()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const layerTreeProps = {
    label: 'label',
    children: 'children'
}
const defaultCheckedKeys = ['global', 'city-base']
const layerTree = ref([
    {
        key: 'base',
        label: '地理信息图层',
        children: [
            { key: 'global', label: '全球地形' },
            { key: 'admin', label: '行政区划' }
        ]
    },
    {
        key: 'city',
        label: '城市CIM图层',
        children: [
            { key: 'city-base', label: '皖山白模' },
            { key: 'city-grid', label: '皖山白模光栅网格' },
            { key: 'city-tilt', label: '皖山倾斜摄影' },
            { key: 'city-tilt-grid', label: '皖山倾斜摄影网格' }
        ]
    },
    {
        key: 'sky',
        label: '空域要素图层',
        children: [
            { key: 'airspace', label: '空域边界' },
            { key: 'route', label: '飞行航路' }
        ]
    }
])
const getDevicePosition = (item) => {
    const longitudeRaw = item.longitude ?? item.lng ?? item.lon
    const latitudeRaw = item.latitude ?? item.lat
    if (longitudeRaw == null || latitudeRaw == null) return null
    const longitude = Number(longitudeRaw)
    const latitude = Number(latitudeRaw)
    if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
    return { longitude, latitude }
}
const clearDeviceEntities = () => {
    if (!viewer) return
    deviceEntityIds.forEach((id) => {
        viewer.entities.removeById(id)
    })
    deviceEntityIds.clear()
}
const RING_STYLES = [
    { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] },
    { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] },
    { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] }
]
const MATERIAL_TYPE = 'RadialGradientMaterial'
let materialRegistered = false
const registerRadialGradientMaterial = () => {
    if (materialRegistered || !Cesium?.Material) return
    materialRegistered = true
    Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
        fabric: {
            type: MATERIAL_TYPE,
            uniforms: {
                color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
                color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0)
            },
            source: `
                czm_material czm_getMaterial(czm_materialInput materialInput) {
                    czm_material material = czm_getDefaultMaterial(materialInput);
                    vec2 st = materialInput.st - vec2(0.5);
                    float t = clamp(length(st) * 2.0, 0.0, 1.0);
                    vec4 color = mix(color1, color2, t);
                    material.diffuse = color.rgb;
                    material.alpha = color.a;
                    return material;
                }
            `
        },
        translucent: () => true
    })
}
class RadialGradientMaterialProperty {
    constructor(color1, color2) {
        this._definitionChanged = new Cesium.Event()
        this.color1 = color1
        this.color2 = color2
    }
    get isConstant () {
        return true
    }
    get definitionChanged () {
        return this._definitionChanged
    }
    getType () {
        return MATERIAL_TYPE
    }
    getValue (time, result) {
        const target = result || {}
        target.color1 = this.color1
        target.color2 = this.color2
        return target
    }
    equals (other) {
        return (
            other instanceof RadialGradientMaterialProperty &&
            Cesium.Color.equals(this.color1, other.color1) &&
            Cesium.Color.equals(this.color2, other.color2)
        )
    }
}
const buildCirclePositions = (center, radiusMeters, steps = 64) => {
    const positions = []
    const latRad = Cesium.Math.toRadians(center.latitude)
    const metersToLat = 1 / 111320
    const metersToLon = 1 / (111320 * Math.cos(latRad))
    for (let i = 0; i <= steps; i += 1) {
        const angle = Cesium.Math.toRadians((i / steps) * 360)
        const dx = radiusMeters * Math.cos(angle)
        const dy = radiusMeters * Math.sin(angle)
        const lon = center.longitude + dx * metersToLon
        const lat = center.latitude + dy * metersToLat
        positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
    }
    return positions
}
const addDeviceRings = (center, baseId) => {
    RING_STYLES.forEach((ring, index) => {
        const outerPositions = buildCirclePositions(center, ring.outer)
        const holes = ring.inner
            ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
            : []
        const entityId = `${baseId}-ring-${index}`
        deviceEntityIds.add(entityId)
        registerRadialGradientMaterial()
        const [startColor, endColor] = ring.gradient
        const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
        const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
        const material = new RadialGradientMaterialProperty(color1, color2)
        viewer.entities.add({
            id: entityId,
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
                material
            }
        })
    })
}
const renderDeviceEntities = (devices) => {
    console.log(devices, 'devices')
    if (!viewer) return
    clearDeviceEntities()
    devices.forEach((item, index) => {
        const position = getDevicePosition(item)
        if (!position) return
        const entityId = `online-device-${item.id ?? index}`
        deviceEntityIds.add(entityId)
        addDeviceRings(position, entityId)
        viewer.entities.add({
            id: entityId,
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            billboard: {
                image: equipmentIcon,
                width: 40.34,
                height: 40.34
            }
        })
    })
}
watch(
    () => props.onlineDevices,
    (devices) => {
        renderDeviceEntities(devices || [])
    },
    { deep: true }
)
watch(
    () => props.leftCollapsed,
    (isCollapsed) => {
        if (isCollapsed) showLayerPanel.value = false
    }
)
const toggleLayerPanel = () => {
    showLayerPanel.value = !showLayerPanel.value
}
const handleClickOutside = (event) => {
    if (!showLayerPanel.value) return
    const target = event.target
    if (layerWrapRef.value?.contains(target)) return
    showLayerPanel.value = false
}
onMounted(() => {
    document.addEventListener('click', handleClickOutside)
    viewInstance = new PublicCesium({
        dom: 'cesium',
        flatMode: false,
        terrain: false,
        layerMode: 4,
        contour: false,
        contour: false
    })
    viewer = viewInstance.getViewer()
})
    renderDeviceEntities(props.onlineDevices)
})
onBeforeUnmount(() => {
    document.removeEventListener('click', handleClickOutside)
    clearDeviceEntities()
    if (viewInstance) {
        viewInstance?.destroy()
        viewInstance?.viewerDestroy()
        viewInstance = null
    }
@@ -38,4 +284,148 @@
    width: 100%;
    height: 100%;
}
</style>
.layer-control-root {
    position: absolute;
    left: 411px;
    bottom: 22px;
    z-index: 9;
    transition: transform 0.3s ease-in-out;
    pointer-events: none;
    &.collapsed {
        transform: translateX(-401px);
    }
}
.layer-control-wrap {
    position: relative;
    display: flex;
    align-items: flex-end;
    pointer-events: auto;
}
.layer-control {
    width: 34px;
    height: 34px;
    cursor: pointer;
    img {
        width: 100%;
        height: 100%;
        display: block;
    }
}
.layer-panel {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 46px;
    bottom: 0;
    width: 160px;
    max-height: 442px;
    background: #191932;
    border-radius: 8px 8px 8px 8px;
    .panel-title {
        padding: 0 16px;
        line-height: 42px;
        font-family: Open Sans, Open Sans;
        font-weight: 400;
        font-size: 12px;
        color: #FFFFFF;
        text-align: left;
        font-style: normal;
        text-transform: none;
        border-bottom: 1px solid rgba(70,70,100,0.5);
        box-sizing: border-box;
    }
    .panel-content {
        padding: 0 16px;
        height: 0;
        flex: 1;
        overflow: auto;
    }
    ::v-deep(.el-tree) {
        background: transparent;
        color: #C3C3DD;
        font-size: 12px;
        .el-tree-node {
            line-height: 30px !important;
            .el-tree-node__content {
                display: flex;
                align-items: center;
                height: 40px !important;
                line-height: 40px !important;
                border-bottom: 1px solid rgba(70,70,100,0.5);
                box-sizing: border-box;
            }
            .el-tree-node__children {
                .el-tree-node__content {
                    border: none;
                }
            }
        }
    }
}
.layer-panel :deep(.el-tree-node__label) {
    color: #C3C3DD;
}
.layer-panel :deep(.el-tree-node__expand-icon) {
    order: 3;
    margin-left: auto;
}
.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
    visibility: hidden;
}
.layer-panel :deep(.el-checkbox) {
    order: 1;
}
.layer-panel :deep(.el-tree-node__label) {
    order: 2;
}
.layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) {
    display: none;
}
.layer-panel :deep(.el-tree-node__content:hover) {
    background: transparent !important;
}
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
    background: transparent !important;
    color: #FFFFFF;
}
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
    color: #FFFFFF;
}
.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
    background-color: #023AFF;
    border-color: #023AFF;
}
.layer-panel :deep(.el-checkbox__inner) {
    background-color: transparent;
    border-color: #A1A3D4;
}
</style>
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-07 15:17:54
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 17:04:11
 * @LastEditTime : 2026-01-09 16:54:31
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -59,7 +59,7 @@
import EmptyState from './EmptyState.vue'
import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png'
import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png'
import { deviceSearchApi } from '@/api/dataCockpit'
import { deviceSearchApi, deviceStatisticsApi } from '@/api/dataCockpit'
const props = defineProps({
    collapsed: {
@@ -68,7 +68,7 @@
    }
})
const emit = defineEmits(['update:collapsed'])
const emit = defineEmits(['update:collapsed', 'update:onlineDevices'])
const collapsedModel = computed({
    get: () => props.collapsed,
@@ -79,13 +79,13 @@
    {
        id: 1,
        name: '侦测设备',
        val: 169,
        val: 0,
        logo: zcsbLogo
    },
    {
        id: 2,
        name: '反制设备',
        val: 169,
        val: 0,
        logo: fzsbLogo
    }
])
@@ -148,6 +148,7 @@
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        counterDeviceStatusList.value = records.map(formatDeviceItem)
        emit('update:onlineDevices', records)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
@@ -159,7 +160,26 @@
onMounted(() => {
    fetchDeviceList()
    fetchDeviceStatistics()
})
const normalizeType = (value) => {
    if (value === 1 || value === '1') return '侦测设备'
    if (value === 2 || value === '2') return '反制设备'
    if (value === 'detect') return '侦测设备'
    if (value === 'counter') return '反制设备'
    return value
}
const fetchDeviceStatistics = async () => {
    const res = await deviceStatisticsApi()
    const list = res?.data?.data ?? []
    const map = new Map(list.map((item) => [normalizeType(item.type), item.count]))
    categoryList.value = categoryList.value.map((item) => ({
        ...item,
        val: map.has(item.name) ? map.get(item.name) : item.val
    }))
}
</script>
<style lang="scss" scoped>
@@ -247,17 +267,12 @@
    position: absolute;
    top: 50%;
    right: 357px;
    background: rgba(17, 23, 34, 0.8);
    border: 1px solid #333355;
    border-right: none;
    border-radius: 4px 0 0 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 10;
    backdrop-filter: blur(5px);
    transform: translateY(-50%);
    .arrow {
applications/drone-command/src/views/dataCockpit/index.vue
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-06 16:35:50
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 09:45:25
 * @LastEditTime : 2026-01-09 16:34:39
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\index.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -10,9 +10,9 @@
-->
<template>
  <div class="page-container">
    <MapContainer />
    <MapContainer :online-devices="onlineDevices" :left-collapsed="leftCollapsed" />
    <LeftContainer class="left-container" v-model:activeId="leftActiveId" v-model:collapsed="leftCollapsed" />  
    <RightContainer class="right-container" v-model:collapsed="rightCollapsed" />
    <RightContainer class="right-container" v-model:collapsed="rightCollapsed" v-model:onlineDevices="onlineDevices" />
    <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" />  
  </div>
</template>
@@ -27,6 +27,7 @@
  const leftCollapsed = ref(false);
  const rightCollapsed = ref(false);
  const onlineDevices = ref([]);
  const onSelectWarning = (type) => {
    leftActiveId.value = type;
@@ -52,7 +53,7 @@
    bottom: 0;
    width: 401px;
    z-index: 9;
    backdrop-filter: blur(5px);
    backdrop-filter: blur(2px);
    ::v-deep(.wrapper) {
      display: flex;
@@ -63,6 +64,7 @@
      padding: 20px;
      width: 300px;
      background: rgba(0,0,0,0.3);
      backdrop-filter: blur(16px);
      border-radius: 10px 10px 10px 10px;
    }
  }