Merge remote-tracking branch 'origin/master'
63 files modified
1 files added
7 files deleted
| | |
| | | # @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. |
| | |
| | | |
| | | #开发环境代理地址(推荐本地新建文件 .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/' |
| | |
| | | <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> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import defaultAva from '@/assets/images/defaultava.png' |
| | | |
| | | import logo from '@/assets/images/topContainer/logo.png' |
| | | |
| | | import { mapGetters } from 'vuex' |
| | |
| | | data () { |
| | | return { |
| | | logoUrl: logo, |
| | | defaultAva: defaultAva |
| | | } |
| | | }, |
| | | filters: {}, |
| | |
| | | height: 21px; |
| | | } |
| | | |
| | | .top-bar__user__img { |
| | | width: 24px; |
| | | height: 24px; |
| | | } |
| | | |
| | | .el-dropdown-link { |
| | | cursor: pointer; |
| | | color: #606266; |
| | |
| | | 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 { |
| | |
| | | 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, |
| | |
| | | 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 }); |
| | |
| | | |
| | | .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 { |
| | |
| | | .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; |
| | |
| | | } |
| | | |
| | | .el-time-panel__footer { |
| | | background: #012350 !important; |
| | | background: #161B2C !important; |
| | | border-top: 1px solid rgba(71, 157, 255, 0.3) !important; |
| | | |
| | | .el-button { |
| | |
| | | td.start-date span, |
| | | td.start-date span, |
| | | td.end-date span { |
| | | background-color: #012350 !important; |
| | | background-color: #161B2C !important; |
| | | } |
| | | |
| | | td.in-range div, |
| | |
| | | } |
| | | |
| | | .el-picker-panel__sidebar { |
| | | background: #012350; |
| | | background: #161B2C; |
| | | color: #fff; |
| | | |
| | | .el-picker-panel__shortcut { |
| | |
| | | |
| | | .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; |
| | | |
| | |
| | | |
| | | .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; |
| | | |
| | |
| | | } |
| | | |
| | | .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; |
| | | } |
| | |
| | | } |
| | | |
| | | .el-select__wrapper { |
| | | padding: 0 8px; |
| | | background: #2E2E48; |
| | | box-shadow: none !important; |
| | | border: none; |
| | |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | |
| | |
| | | .el-button { |
| | | min-width: 88px; |
| | | } |
| | | |
| | | .el-button.is-disabled, .el-button.is-disabled:hover { |
| | | background: #1A2652; |
| | | border: none; |
| | | } |
| | | } |
| | | |
| | | .ztzf-table-container { |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | .ztzf-page-view-dialog { |
| | | padding: 30px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 770px; |
| | |
| | | } |
| | | |
| | | .dialog-form { |
| | | |
| | | .el-form-item { |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | .el-form-item__content { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | |
| | | .el-input { |
| | | width: 0; |
| | | flex: 1; |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | }) |
| | | 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) // 查询表单实例 |
| | |
| | | <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> |
| | |
| | | |
| | | 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) // 弹框实例 |
| | |
| | | <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> |
| | | |
| | |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | 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) { |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | 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) { |
| | |
| | | } |
| | | |
| | | // 获取场景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() |
| | |
| | | } |
| | | |
| | | // 格式化防区面积 |
| | | 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, |
| | |
| | | } |
| | | |
| | | // 新增模式绘制 |
| | | 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() |
| | |
| | | } |
| | | |
| | | // 查看面 |
| | | function viewPolygon() { |
| | | function viewPolygon () { |
| | | if (!formData.value?.geom) return |
| | | pointList = geomAnalysis(formData.value.geom) |
| | | const result = pointList.map(item => [item.longitude, item.latitude]).flat() |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | async function open ({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | formData.value = dialogMode.value === 'add' ? initForm() : row |
| | | selectedSceneRows.value = [] |
| | |
| | | }) |
| | | </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> |
| | |
| | | |
| | | <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)"> |
| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | |
| | | 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) // 查询表单实例 |
| | |
| | | <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"> |
| | |
| | | </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> |
| | | |
| | |
| | | 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('') |
| | |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | 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 |
| | |
| | | ) |
| | | |
| | | // 加载详情 |
| | | 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() |
| | |
| | | } |
| | | |
| | | // 格式化区域位置 |
| | | 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, |
| | |
| | | } |
| | | |
| | | // 新增模式绘制 |
| | | 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() |
| | |
| | | } |
| | | |
| | | // 查看面 |
| | | function viewPolygon() { |
| | | function viewPolygon () { |
| | | if (!formData.value?.geom) return |
| | | pointList = geomAnalysis(formData.value.geom) |
| | | const result = pointList.map(item => [item.longitude, item.latitude]).flat() |
| | |
| | | } |
| | | |
| | | // 获取派出所列表 |
| | | 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 ?? [] |
| | |
| | | } |
| | | |
| | | // 关联设备变更 |
| | | 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 = [] |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | async function open ({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | formData.value = dialogMode.value === 'add' ? initForm() : row |
| | | selectedDeviceRows.value = [] |
| | |
| | | }) |
| | | </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> |
| | |
| | | |
| | | <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)"> |
| | |
| | | {{ 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> |
| | |
| | | </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' |
| | |
| | | |
| | | 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) // 查询表单实例 |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | 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) |
| | |
| | | } |
| | | } |
| | | |
| | | 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({ |
| | |
| | | }) |
| | | 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 }) |
| | |
| | | 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) |
| | |
| | | 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()) |
| | |
| | | defineExpose({ open }) |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | #mapContainer { |
| | | width: 100%; |
| | | height: 500px; |
| | | } |
| | | </style> |
| | |
| | | |
| | | <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)"> |
| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | }) |
| | | 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) // 查询表单实例 |
| | |
| | | <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' |
| | |
| | | const initForm = () => ({ |
| | | sceneName: '', // 场景名称 |
| | | sceneType: '', // 场景类型 |
| | | defenseLeader: '', // 防控负责人 |
| | | leaderPhone: '', // 防控负责人电话 |
| | | defenseLeader: '', // 负责人 |
| | | leaderPhone: '', // 负责人电话 |
| | | deviceMode: '', // 设备模式 |
| | | areaDivideIds: '', // 关联区域ID |
| | | areaCount: 0, // 区域数量 |
| | |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 渲染面 |
| | | function renderingSurface() { |
| | | function renderingSurface () { |
| | | geometricSource && geometricSource.entities.removeAll() |
| | | selectedAreaRows.value.forEach(item => { |
| | | if (item.geom) { |
| | |
| | | |
| | | 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 = [] |
| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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({ |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | async function open ({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | formData.value = dialogMode.value === 'add' ? initForm() : row |
| | | selectedAreaRows.value = [] |
| | |
| | | }) |
| | | </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> |
| | |
| | | |
| | | <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)"> |
| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | |
| | | 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) // 查询表单实例 |
| | |
| | | <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> |
| | |
| | | }) |
| | | 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) // 查询表单实例 |
| | |
| | | formData.value = initForm() |
| | | } |
| | | const total = ref(0) // 总条数 |
| | | const loading = ref(false) // 列表加载中 |
| | | const loading = ref(true) // 列表加载中 |
| | | const list = ref([]) // 列表数据 |
| | | async function getList () { |
| | | loading.value = true |
| | |
| | | |
| | | <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> |
| | | |
| | |
| | | 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列表 |
| | |
| | | }) |
| | | 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 |
| | |
| | | |
| | | <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)"> |
| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | }) |
| | | 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) // 查询表单实例 |
| | |
| | | }; |
| | | |
| | | // 地图接口 |
| | | const loading = ref(false); |
| | | const loading = ref(true); |
| | | const getMapInfoAPIFun = async ids => { |
| | | try { |
| | | const res = await getMapInfoAPI(ids); |
| | |
| | | }) |
| | | 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([ |
| | |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const equipmentWarningData = ref([]) |
| | | const loading = ref(false) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const statusMap = { |
| | |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const historyWarningData = ref([]) |
| | | const loading = ref(false) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | // 查询条件:keyword + dateRange(YYYY-MM-DD数组) |
| | |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | const realWarningData = ref([]) |
| | | const loading = ref(false) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const actionTextMap = { |
| | |
| | | |
| | | const historyVisible = ref(false) |
| | | const historyDevice = ref(null) |
| | | const loading = ref(false) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const onHistory = (item) => { |
| | |
| | | <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> |
| | |
| | | |
| | | // 注入字典数据 |
| | | const dictObj = inject('dictObj') |
| | | // 注入反制效果选项 |
| | | |
| | | const formData = ref(initForm()) // 表单数据 |
| | | const visible = defineModel() // 弹框显隐 |
| | |
| | | // 获取反制方式标签 |
| | | function getCounterWayLabel(value) { |
| | | const map = { '1': '信号干扰', '2': '诱导驱离' } |
| | | return map[value] || value || '无' |
| | | return map[value] || value || '暂无' |
| | | } |
| | | |
| | | // 获取设备类型标签 |
| | |
| | | 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 }) |
| | |
| | | <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" /> |
| | |
| | | </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' |
| | |
| | | |
| | | 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) |
| | | |
| | | // 获取列表 |
| | |
| | | <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 |
| | |
| | | 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 '编辑侦测范围' |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | 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, |
| | |
| | | } |
| | | |
| | | // 关闭后重置 |
| | | 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() |
| | |
| | | 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> |
| | |
| | | <template> |
| | | <template> |
| | | <basic-container> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search"> |
| | | <el-form-item label="名称" prop="deviceName"> |
| | |
| | | <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> |
| | |
| | | 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) // 查询表单实例 |
| | |
| | | |
| | | // 删除 |
| | | 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() |
| | |
| | | <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" |
| | |
| | | </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> |
| | |
| | | <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 = [ |
| | |
| | | { label: '扇区', value: '扇区' }, |
| | | { label: '定位', value: '定位' }, |
| | | ] |
| | | const servoDataModeOptions = [{ label: '编码器', value: '编码器' }] |
| | | const servoDataModeOptions = [{ label: '编码模式', value: '编码模式' }] |
| | | const axisSelectOptions = [{ label: '方位', value: '方位' }] |
| | | |
| | | const rules = { |
| | |
| | | visible.value = false |
| | | } |
| | | |
| | | function handleClosed() { |
| | | formData.value = initForm() |
| | | } |
| | | |
| | | // 提交更新 |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | |
| | | <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" /> |
| | |
| | | </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' |
| | |
| | | |
| | | 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) // 弹框实例 |
| | |
| | | <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> |
| | |
| | | <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 |
| | |
| | | </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="至" |
| | |
| | | </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}` : '选择区域后自动带出' |
| | |
| | | <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" |
| | |
| | | <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" |
| | |
| | | <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" |
| | |
| | | </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> |
| | | |
| | |
| | | </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' |
| | |
| | | visible.value = false |
| | | } |
| | | |
| | | function handleClosed() { |
| | | formData.value = initForm() |
| | | } |
| | | |
| | | // 提交新增/编辑 |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | |
| | | <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 |
| | |
| | | <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 |
| | |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="设备编号" prop="deviceSn"> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | |
| | | |
| | | <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)"> |
| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | }) |
| | | 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) // 查询表单实例 |
| | |
| | | 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, |
| | |
| | | }) |
| | | const isShowAddTask = defineModel('show') |
| | | |
| | | const loading = ref(false) |
| | | const loading = ref(true) |
| | | |
| | | // 禁用当天之前的日期 |
| | | const disabledDate = time => { |
| | |
| | | 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) |
| | | } |
| | |
| | | dkbh: '', |
| | | patchesName: '', |
| | | }); |
| | | const loading = ref(false); |
| | | const loading = ref(true); |
| | | const treeData = ref([]); |
| | | const treeAllData = ref([]); |
| | | // 存储过滤后显示的节点ID(返回true的节点) |
| | |
| | | geo_data: '', |
| | | |
| | | }); |
| | | const loading = ref(false); |
| | | const loading = ref(true); |
| | | const detailData = ref(null); |
| | | const fenceArea = ref(0); |
| | | detailData.value = layerParams.value.editDetailData; |
| | |
| | | </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)"> |
| | |
| | | <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> |
| | |
| | | }) |
| | | 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) // 弹框实例 |
| | |
| | | <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' |
| | |
| | | }) |
| | | |
| | | // 关闭弹框 |
| | | 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 }) |
| | |
| | | } |
| | | |
| | | // 角度转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') |
| | |
| | | } |
| | | |
| | | // 关闭后重置 |
| | | 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, |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | 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 { |
| | |
| | | |
| | | 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> |
| | |
| | | </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)"> |
| | |
| | | <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> |
| | |
| | | }) |
| | | 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([]) |
| | |
| | | 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([]); |
| | |
| | | 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) |
| | |
| | | }); |
| | | 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: '' |
| | |
| | | <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> |
| | | |
| | |
| | | /** ✅ 你的 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 |
| | |
| | | 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' }], |
| | | }) |
| | |
| | | /* ---------------- 信令处理 ---------------- */ |
| | | |
| | | async function onSignal(msg) { |
| | | console.log('1cesss',msg); |
| | | |
| | | if (!msg) return |
| | | const t = (msg.type || '').toString() |
| | | |
| | |
| | | |
| | | state.value = 'ringing' |
| | | log('📞 收到来电 call,来自', incomingFrom) |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone() |
| | | startVibration() |
| | | |
| | | return |
| | | } |
| | | |
| | |
| | | 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> |
| | |
| | | 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 } |
| | | ) |
| | | } |
| | |
| | | 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"; |
| | | |
| | |
| | | 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", |
| | | }); |
| | |
| | | // 构建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 |
| | |
| | | export function getAssetsImage (targetUrl) { |
| | | const url = getEnvObj().VITE_APP_ASSETS_URL |
| | | return `${url}${targetUrl}` |
| | | } |
| | | } |
| New file |
| | |
| | | // 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() |
| | |
| | | url: `/system/sys/SysRegionCode/allTree`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | } |
| | |
| | | data: data, |
| | | }); |
| | | }; |
| | | |
| | | // 验证码 |
| | | export const registerSendCodeApi = (phone) => { |
| | | return request({ |
| | | url: `/system/verificationCode/registerSendCode/${phone}`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | <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> |
| | |
| | | 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) |
| | | } |
| | |
| | | url: '/subPackages/deviceRegistration/add', |
| | | }) |
| | | } |
| | | function scrolltolower() { |
| | | formParams.value.current++ |
| | | // 停止不刷新 |
| | | if (equipmentList.value.length < formParams.value.size) { |
| | | return |
| | | } |
| | | getEquipmentList() |
| | | } |
| | | onMounted(() => { |
| | | getEquipmentList() |
| | | }) |
| | |
| | | <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> |
| | |
| | | }, |
| | | ]) |
| | | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | url: '/subPackages/regulationsDetail/details?id=' + item.noticeId+'&type=2', |
| | | }) |
| | | } |
| | | |
| | | |
| | | |
| | | onMounted(() => { |
| | | getSysNoticeList() |
| | |
| | | <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" /> --> |
| | |
| | | </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> |
| | |
| | | </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: '', |
| | |
| | | } |
| | | |
| | | // 这里假设账号就是手机号,实际项目中可能需要验证格式 |
| | | sendResetCode(formParams.value.userName).then(res => { |
| | | console.log('4444',res) |
| | | registerSendCodeApi(formParams.value.userName).then(res => { |
| | | if (res.data.code === 200) { |
| | | uni.showToast({ |
| | | title: '验证码发送成功', |
| | |
| | | title: '注册成功', |
| | | icon: 'success', |
| | | }); |
| | | // 跳转登录页面 |
| | | uni.navigateTo({ |
| | | url: '/pages/login/index' |
| | | }) |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.data.msg || '注册失败', |
| | |
| | | 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 { |
| | |
| | | font-weight: 400; |
| | | font-size: 15px; |
| | | color: #222324; |
| | | .nike { |
| | | margin-right: 16rpx; |
| | | } |
| | | .required { |
| | | color: #FF4444; |
| | | } |
| | |
| | | <!-- 我的 --> |
| | | <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> |
| | | |
| | |
| | | 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(); |
| | | // |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | .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%; |
| | | } |
| | |
| | | <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="民用航空器登记证书" |
| | |
| | | /> |
| | | <!-- 所属区域 --> |
| | | <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> |
| | |
| | | hasInsurance: '', // 是否有保险 |
| | | hasInsuranceText: '', // 是否有保险文本 |
| | | region: '', // 所属区域 |
| | | regionCode: '', // 所属区域 |
| | | regionText: '', // 所属区域文本 |
| | | caacRegistrationCode: '', // 民用航空器登记证书 |
| | | caacRegistrationCodeText: '', // 民用航空器登记证书文本 |
| | | deviceImage: '', // 飞行器照片 |
| | |
| | | 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([ |
| | |
| | | // 获取所属单位 |
| | | 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) { |
| | | // 获取上传的文件对象 |
| | |
| | | // 这里可以添加额外的失败处理逻辑 |
| | | } |
| | | } |
| | | 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"> |
| | |
| | | </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> |
| | |
| | | } |
| | | |
| | | // 审批记录是很多条 |
| | | 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, |
| | |
| | | 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() |
| | |
| | | .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 { |
| | |
| | | width: 90%; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | top: 340rpx !important; |
| | | top: 350rpx !important; |
| | | height: 68rpx; |
| | | flex: 0; |
| | | } |
| | |
| | | } |
| | | } |
| | | .fj-list { |
| | | margin-top: 40rpx; |
| | | // margin-top: 20rpx; |
| | | } |
| | | .fj-item { |
| | | display: flex; |
| | |
| | | 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; |
| | |
| | | </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"; |
| | |
| | | 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 { |
| | |
| | | font-weight: 400; |
| | | font-size: 30rpx; |
| | | color: #222324; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | input.input-item { |