无人机管理后台前端(已迁走)
罗广辉
2025-08-08 46cdda27d21f30b4e87018e76e438d11c75c7108
feat: 下载逻辑
15 files modified
4 files deleted
4 files added
464 ■■■■■ changed files
.env.development 8 ●●●● patch | view | raw | blame | history
.env.jiangwu 4 ●●●● patch | view | raw | blame | history
.env.localhost 2 ●●● patch | view | raw | blame | history
.env.production 2 ●●● patch | view | raw | blame | history
.env.test 2 ●●● patch | view | raw | blame | history
src/App.vue 3 ●●●●● patch | view | raw | blame | history
src/api/dataCenter/dataCenter.js 17 ●●●●● patch | view | raw | blame | history
src/event-bus/index.js 14 ●●●●● patch | view | raw | blame | history
src/hooks/use-connect-websocket.js 20 ●●●●● patch | view | raw | blame | history
src/page/index/index.vue 1 ●●●● patch | view | raw | blame | history
src/page/index/useDownloadWs.js 48 ●●●●● patch | view | raw | blame | history
src/store/modules/common.js 7 ●●●●● patch | view | raw | blame | history
src/utils/util.js 14 ●●●●● patch | view | raw | blame | history
src/utils/websocket/config.js 11 ●●●●● patch | view | raw | blame | history
src/utils/websocket/connect-websocket.js 16 ●●●●● patch | view | raw | blame | history
src/utils/websocket/index.js 76 ●●●●● patch | view | raw | blame | history
src/views/dataCenter/components/searchData.vue 80 ●●●●● patch | view | raw | blame | history
src/views/dataCenter/dataCenter.vue 15 ●●●●● patch | view | raw | blame | history
src/views/device/addDevice.vue 2 ●●●●● patch | view | raw | blame | history
src/views/device/airport.vue 8 ●●●● patch | view | raw | blame | history
src/views/device/components/DockControlPanel.vue 18 ●●●●● patch | view | raw | blame | history
src/websocket/index.js 77 ●●●●● patch | view | raw | blame | history
src/websocket/util/config.js 19 ●●●●● patch | view | raw | blame | history
.env.development
@@ -4,10 +4,10 @@
 # @LastEditors  : yuan
 # @LastEditTime : 2025-07-29 16:15:31
 # @FilePath     : \.env.development
 # @Description  :
 # Copyright 2025 OBKoro1, All Rights Reserved.
 # @Description  :
 # Copyright 2025 OBKoro1, All Rights Reserved.
 # 2025-07-29 15:59:42
###
###
NODE_ENV = 'development'
#开发环境配置
@@ -30,7 +30,7 @@
VITE_APP_AREA_NAME = https://wrj.shuixiongit.com
# ws地址
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/minio/cloud-bucket
.env.jiangwu
@@ -22,7 +22,7 @@
VITE_APP_DASHBOARD_URL = 'http://192.168.253.121:8080/command-center-dashboard/'
# ws地址
VITE_APP_WS_API_URL = ws://192.168.253.121:8080
VITE_APP_WS_API_URL = ws://192.168.253.121:8080/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = http://192.168.253.121:9000/cloud-bucket
@@ -45,4 +45,4 @@
VITE_APP_DEVICE_IP = '139.196.74.78:1883'
VITE_APP_TDT_TOKEN = e110584a27d506da2740edca951683f4
VITE_APP_CESIUM_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTZlNGNlYS01NTU1LTQ1MGEtYmNlZS0yNTE2NDk5YWM2MjEiLCJpZCI6MTc5Njk2LCJpYXQiOjE3MDA1NDcwMjV9.qcl4AH2731cfFd0-I1ZLUINPXqvglLkDFD-UGR2zU5M
VITE_APP_CESIUM_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTZlNGNlYS01NTU1LTQ1MGEtYmNlZS0yNTE2NDk5YWM2MjEiLCJpZCI6MTc5Njk2LCJpYXQiOjE3MDA1NDcwMjV9.qcl4AH2731cfFd0-I1ZLUINPXqvglLkDFD-UGR2zU5M
.env.localhost
@@ -20,7 +20,7 @@
VITE_APP_AREA_NAME = http://192.168.1.227:80
# ws地址
VITE_APP_WS_API_URL = wss://192.168.1.227:80
VITE_APP_WS_API_URL = wss://192.168.1.227:80/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = http://192.168.1.227:80/minio/cloud-bucket
.env.production
@@ -22,7 +22,7 @@
VITE_APP_DASHBOARD_URL = 'https://aisky.org.cn/command-center-dashboard/'
# ws地址
VITE_APP_WS_API_URL = wss://aisky.org.cn
VITE_APP_WS_API_URL = wss://aisky.org.cn/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/aiskyminio/cloud-bucket
.env.test
@@ -20,7 +20,7 @@
VITE_APP_AREA_NAME = https://wrj.shuixiongit.com
# ws地址
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/minio/cloud-bucket
src/App.vue
@@ -2,7 +2,10 @@
  <router-view />
</template>
<script setup>
import { useDownloadWs } from '@/page/index/useDownloadWs';
useDownloadWs()
</script>
<style>
src/api/dataCenter/dataCenter.js
@@ -35,6 +35,23 @@
        data
    })
}
// 查询下载状态
export const getDownloadStatusApi = (data) => {
    return request({
        url: `/blade-resource/attach/getDownloadStatus`,
        method: 'post',
        data
    })
}
// 取消下载
export const cancelDownloadApi = (data) => {
    return request({
        url: `/blade-resource/attach/cancelDownload`,
        method: 'post',
        data
    })
}
// 地图
export const getMapInfoAPI = (jobId) => {
    return request({
src/event-bus/index.js
File was deleted
src/hooks/use-connect-websocket.js
File was deleted
src/page/index/index.vue
@@ -27,7 +27,6 @@
    <!-- <wechat></wechat> -->
  </div>
</template>
<script>
import index from '@/mixins/index'
import wechat from './wechat.vue'
src/page/index/useDownloadWs.js
New file
@@ -0,0 +1,48 @@
import { getWebsocketUrl } from '@/utils/websocket/config'
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket'
import { useStore } from 'vuex'
import dayjs from 'dayjs'
import { aLinkDownloadUtil } from '@/utils/util'
import EventBus from '@/utils/eventBus'
import { computed, onBeforeUnmount, onMounted } from 'vue';
import { setStore,getStore } from '@/utils/store';
import { getDownloadStatusApi } from '@/api/dataCenter/dataCenter';
export const useDownloadWs = () => {
    const store = useStore()
    const loginUserInfo = computed(() => store.state.user.userInfo)
    const downloadProgress = computed(() => store.state.common.downloadProgress)
    let currentWebSocket = null
    function messageHandler(payload) {
        const {type,status,progress,download_url} = payload
        const setProgress = status === 'CANCELLED'
            ? 100
            : (status !== 'COMPLETED' && progress === 100) ? 99 : progress
        store.commit('setDownloadProgress', {
            ...downloadProgress.value,
            [type]: setProgress,
        })
        if (status === 'COMPLETED' && ['htsjzx','htlsrwxq'].includes(type)) {
            const name = `数据中心-${dayjs().format('YYYYMMDDHHmmss')}.zip`
            const currentUrl = getStore({name: 'downloadUrl'})
            if (currentUrl === download_url && download_url!== undefined) return
            aLinkDownloadUtil(download_url, name)
            setStore({ name: 'downloadUrl', content: download_url })
        }
    }
    onMounted(() => {
        let webSocketUrl = getWebsocketUrl() + `&model_type=3&workspace-id=${loginUserInfo.value.user_id}`
        currentWebSocket = useConnectWebSocket((result)=>{
            const payload = JSON.parse(result)
            messageHandler(payload.data)
        }, webSocketUrl)
        EventBus.on('useDownloadWs-messageHandler',messageHandler)
    })
    onBeforeUnmount(() => {
        currentWebSocket?.close()
        EventBus.off('useDownloadWs-messageHandler',messageHandler)
    })
    return {}
}
src/store/modules/common.js
@@ -15,8 +15,15 @@
    lockPasswd: getStore({ name: 'lockPasswd' }) || '',
    website: website,
    setting: website.setting,
    downloadProgress:{
      htsjzx: 100, //数据中心
      htlsrwxq: 100, //历史任务详情
    }
  },
  mutations: {
    setDownloadProgress(state, data) {
      state.downloadProgress = data
    },
    SET_LANGUAGE: (state, language) => {
      state.language = language;
      setStore({
src/utils/util.js
@@ -477,4 +477,16 @@
    }
  }
  return newObj
}
}
// a链接直接下载
export const aLinkDownloadUtil = (url, name) => {
  let a = document.createElement('a')
  a.style.display = 'none'
  a.href = url
  a.download = name
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  a = null
}
src/utils/websocket/config.js
New file
@@ -0,0 +1,11 @@
import { getToken } from '@/utils/auth';
export function getWebsocketUrl() {
  const token = getToken() || '';
  return import.meta.env.VITE_APP_WS_API_URL + '?x-auth-token=' + encodeURI(token);
}
// AI 指令飞行
export function getAIWebsocketUrl() {
    const token = getToken() || ''
    return `${import.meta.env.VITE_APP_WS_API_URL}?x-auth-token=${encodeURI(token)}`;
}
src/utils/websocket/connect-websocket.js
New file
@@ -0,0 +1,16 @@
import ConnectWebSocket from '@/utils/websocket/index'
import { getWebsocketUrl } from '@/utils/websocket/config'
/**
 * 接收一个message函数
 * @param messageHandler
 * @param url
 */
export function useConnectWebSocket(messageHandler, url) {
    const webSocket = new ConnectWebSocket(url || getWebsocketUrl())
    webSocket?.registerMessageHandler(messageHandler)
    webSocket?.initSocket()
    return webSocket
}
src/utils/websocket/index.js
New file
@@ -0,0 +1,76 @@
import ReconnectingWebSocket from 'reconnecting-websocket'
/**
 * ConnectWebSocket 类
 * TODO: 优化messageHandler: EventEmitter。暂时传入回调函数
 */
class ConnectWebSocket {
    _url
    _socket
    _hasInit
    _messageHandler
    constructor(url) {
        this._url = url
        this._socket = null
        this._hasInit = false
        this._messageHandler = null
    }
    initSocket() {
        if (this._hasInit) {
            return
        }
        if (!this._url) {
            return
        }
        // 会自动重连,无需处理重连逻辑
        this._socket = new ReconnectingWebSocket(this._url, [], {
            maxReconnectionDelay: 20000, // 断开后最大的重连时间: 20s,每多一次重连,会增加 1.3 倍,5 * 1.3 * 1.3 * 1.3...
            minReconnectionDelay: 5000, // 断开后最短的重连时间: 5s
            maxRetries: 5,
        })
        this._hasInit = true
        this._socket.addEventListener('open', this._onOpen.bind(this))
        this._socket.addEventListener('close', this._onClose.bind(this))
        this._socket.addEventListener('error', this._onError.bind(this))
        this._socket.addEventListener('message', this._onMessage.bind(this))
    }
    _onOpen() {
        console.log('ws连接成功')
    }
    _onClose() {
        console.log('ws连接已断开')
    }
    _onError() {
        console.log('ws连接 error')
    }
    registerMessageHandler(messageHandler) {
        this._messageHandler = messageHandler
    }
    _onMessage(msg) {
        // const data = JSON.parse(msg.data);
        this._messageHandler && this._messageHandler(msg.data)
    }
    sendMessage = message => {
        // 目前只供AI聊天使用
        this._socket?.send(message)
        // this._socket?.send(JSON.stringify(message.data));
    }
    close() {
        this._socket?.close()
        this._messageHandler = null
        this._socket = null
    }
}
export default ConnectWebSocket
src/views/dataCenter/components/searchData.vue
@@ -101,12 +101,18 @@
      </div>
      <div class="search-first">
        <div class="search-btn">
          <el-button type="primary" icon="el-icon-download" @click="allDownloadFun"
            >全部下载</el-button
          >
          <el-button type="success" plain icon="el-icon-download" @click="downloadFun"
            >下载</el-button
          >
<!--          <el-button type="primary" icon="el-icon-download" @click="allDownloadFun"-->
<!--            >全部下载</el-button-->
<!--          >-->
          <div class="downloadBtn" @click="htsjzx === 100 && downloadFun()">
            <el-progress v-if="htsjzx !== 100" :percentage="htsjzx" :show-text="false" striped striped-flow :duration="1" />
            <div class="downloadBtnText">
              <span v-if="htsjzx === 100">下载</span>
              <template v-else>
                处理中<el-icon @click="cancelDownload"><CircleClose /></el-icon>
              </template>
            </div>
          </div>
        </div>
      </div>
    </el-form>
@@ -120,9 +126,13 @@
import { useStore } from 'vuex';
import { getRegionTreeAll, getDeviceRegion, deptsByAreaCode } from '@/api/job/task';
import { watch } from 'vue';
import { cancelDownloadApi, getDownloadStatusApi } from '@/api/dataCenter/dataCenter';
import EventBus from '@/utils/eventBus';
const store = useStore();
const userAreaCode = computed(() => store.getters.userInfo.detail.areaCode);
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode);
const htsjzx = computed(() => store.state.common.downloadProgress?.htsjzx || 100)
const emit = defineEmits(['search', 'downFun', 'allDownFun']);
const startTime = dayjs().subtract(6, 'day').startOf('day');
const endTime = dayjs().endOf('day');
@@ -165,7 +175,7 @@
  {
    value: '5',
    label: '全景',
  },
  {
    value: '4',
@@ -301,8 +311,20 @@
const allDownloadFun = () => {
  emit('allDownFun');
};
function cancelDownload() {
  cancelDownloadApi({ type:'htsjzx' }).then(res =>{
    ElMessage.success('取消成功')
  })
}
onMounted(() => {
  requestDockInfo();
  getDownloadStatusApi({type: 'htsjzx'}).then(res =>{
    if (!['CANCELLED','COMPLETED'].includes(res.data.data?.status || 'COMPLETED')){
      EventBus.emit('useDownloadWs-messageHandler',res.data.data)
    }
  })
});
</script>
@@ -320,6 +342,50 @@
    .search-btn {
      margin-right: 32px;
      .downloadBtn{
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        height: 28px;
        padding: 0 15px;
        background: rgb(239.8,248.9,235.3);
        border-radius: 4px 4px 4px 4px;
        border: 1px solid rgb(179,224.5,156.5);
        cursor: pointer;
        color: #67c23a;
        font-size: 14px;
        .downloadBtnText{
          display: flex;
          align-items: center;
          gap: 5px;
          z-index: 5;
        }
        .el-progress{
          height: 100%;
          position: absolute;
          left: 0;
          top: 0;
          width: 100%;
          :deep(){
            .el-progress-bar__outer{
              height: 26px !important;
              border-radius: 0 !important;
              background: transparent !important;
              .el-progress-bar__inner{
                border-radius: 0 !important;
                background-color: #e5ffd9;
              }
            }
          }
        }
      }
    }
    .time-card {
      text-align: center;
src/views/dataCenter/dataCenter.vue
@@ -306,7 +306,7 @@
  deleteFileMultipleApi,
  downloadApi,
  updataTitleApi,
  getOrthoimageInfo,
  getOrthoimageInfo, getDownloadStatusApi
} from '@/api/dataCenter/dataCenter';
import { getShowImg, getSmallImg, getzsSmallImg } from '@/utils/util';
import { onMounted, watch } from 'vue';
@@ -482,7 +482,7 @@
  a.click();
  document.body.removeChild(a);
}
const fileDownload = () => {
const fileDownload = async () => {
  const list = selectedRows.value.filter(i => i.checked);
  if (!list?.length) return ElMessage.warning('请选择文件');
  if (list.length === 1) {
@@ -503,11 +503,14 @@
      jobName: '',
      resultType: '',
      startTime: '',
      wayLineJobIds: [],
      type:'htsjzx'
    };
    downloadApi(aaa).then(res => {
      aLinkDownload(res.data.data, `sjzx-file-pack-${dayjs().format('YYYYMMDDHHmmss')}.zip`);
    const res = await getDownloadStatusApi({type: 'htsjzx'})
    if (!['CANCELLED','COMPLETED'].includes(res.data.data?.status || 'COMPLETED')){
      loading.close()
      return ElMessage.warning('还有正在处理的')
    }
    downloadApi(aaa).finally(res => {
      loading.close();
    });
  }
src/views/device/addDevice.vue
@@ -27,8 +27,6 @@
import { getLazyTree } from '@/api/base/region'
import { mapGetters } from 'vuex'
import { getWebsocketUrl } from '@/websocket/util/config'
import ConnectWebSocket from '@/websocket'
import { $Clipboard } from "@smallwei/avue"
src/views/device/airport.vue
@@ -154,8 +154,8 @@
import FirmwareManage from './components/firmwareManage.vue'
import DevicePerShare from './components/devicePerShare.vue'
import DockControlPanel from './components/DockControlPanel.vue'
import { getWebsocketUrl } from '@/websocket/util/config'
import ConnectWebSocket from '@/websocket'
import { getWebsocketUrl } from '@/utils/websocket/config';
import ConnectWebSocket from '@/utils/websocket';
export default {
  components: {
    FirmwareManage,
@@ -584,10 +584,10 @@
        this.webSocketIdSet.add(webSocketId)
        const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + data.workspace_id
        // 监听ws 消息
        this.useConnectWebSocket(webSocketId, webSorketUrl)
        this.useConnectWebSocket1(webSocketId, webSorketUrl)
      }
    },
    useConnectWebSocket (webSocketId, url) {
    useConnectWebSocket1 (webSocketId, url) {
      const websocket = new ConnectWebSocket(url)
      // 加入 webscoket map
      this.websocketMap.set(webSocketId, websocket)
src/views/device/components/DockControlPanel.vue
@@ -77,10 +77,7 @@
<script setup>
import { ElMessage } from 'element-plus';
import EventBus from '@/event-bus'
import { EBizCode, ELocalStorageKey, ERouterName } from '@/types'
import { useConnectWebSocket } from '@/hooks/use-connect-websocket'
import { getWebsocketUrl } from '@/websocket/util/config'
import { defineProps, ref, watch, reactive } from 'vue'
import { cmdList as baseCmdList } from '@/types/device-cmd'
import { useDockControl } from './use-dock-control'
@@ -89,6 +86,9 @@
import { setThermalCurrentPaletteStyle, setPhotoStorageSet, setVideoStorageSet, getLiveStatus, setStreamsSwitch, photoAndVideoCmd } from '@/api/device-setting'
import Store from '@/store'
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket';
import { getWebsocketUrl } from '@/utils/websocket/config';
import EventBus from '@/utils/eventBus';
const valueStyle = ref(0)
const cameraModeValue = ref(0)
@@ -248,6 +248,9 @@
  }
)
onMounted(() => {
  const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + props.deviceInfo.workspace_id
  // 监听ws 消息
  useConnectWebSocket(messageHandler, webSorketUrl)
  getLiveStatuss()
});
@@ -292,13 +295,11 @@
    action: cmdItem.action,
  }
  const success = await sendDockControlCmd(params, true)
  if (success) {
    updateDeviceSingleCmdInfo(cmdList.value[index])
  }
}
// webSocket 监听
const messageHandler = async payload => {
  payload = JSON.parse(payload)
  if (!payload) {
    return
  }
@@ -384,9 +385,6 @@
  }
}
const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + props.deviceInfo.workspace_id
// 监听ws 消息
useConnectWebSocket(messageHandler, webSorketUrl)
// 添加 changs 方法
async function changs(value, item) {
@@ -532,4 +530,4 @@
    }
  }
}
</style>
</style>
src/websocket/index.js
File was deleted
src/websocket/util/config.js
File was deleted