forked from drone/command-center-dashboard

罗广辉
2025-04-21 2800fa4f32f3900509cb4d6eefaf2bfaf54efdd7
src/components/CurrentTaskDetails/TaskDetailsLeft.vue
@@ -3,7 +3,7 @@
      <div class="title">负载控制</div>
      <div
         class="singleCol"
         v-for="item in list1"
         v-for="item in list1.filter(i => i.show)"
         @click="cameraType(item)"
         :key="item.key"
         :class="{ active: item.key === cameraParams.camera_type }"
@@ -21,11 +21,13 @@
         <div @click="takePictures">拍照</div>
         <div @click="recordFun">{{ isRecording ? '录像中...' : '录像' }}</div>
      </div>
      <div class="multiCol">
         <div @click="ElMessage.warning('加急开发中...')">喊话</div>
         <div @click="ElMessage.warning('加急开发中...')">广播</div>
      <div class="multiCol" v-if="wsInfo?.psdk_widget_values">
         <div @click="shoutFun">{{ isRecordShouting ? '喊话' : '停止喊话' }}</div>
         <div @click="broadcastFun">广播</div>
      </div>
      <div class="percent" v-if="isRecordShouting && percent > 0">
         <el-progress :text-inside="true" :stroke-width="10" :percentage="percent"></el-progress>
      </div>
      <div class="cameraZoom">
         <el-slider
            v-model="cameraParams.zoom_factor"
@@ -34,23 +36,84 @@
            :max="cameraParams.camera_type === 'ir' ? 200 : 200"
            @change="sliderChange"
         />
         <div class="cameraZoomText">{{cameraParams.zoom_factor}}X</div>
      </div>
   </div>
   <!-- 广播列表 -->
   <div class="broadcast" v-if="isBroadcast">
      <div class="title">
         <span>语音内容</span>
         <img src="@/assets/images/taskManagement/taskIntermediateContent/broadcast-list.png" alt="" />
         <img @click="isBroadcast = !isBroadcast" class="close" src="@/assets/images/taskManagement/taskIntermediateContent/close.png" alt="">
      </div>
      <div class="search-btn">
         <div class="input-set">
            <label>文件名称:</label><el-input v-model="searchParams.name" placeholder="请输入广播名称"></el-input>
         </div>
         <div class="btn-set">
            <div class="btn" @click="clearData">清空</div>
            <div class="btn" @click="getFileList">搜索</div>
            <el-upload
               class="btn"
               action="/upload"
               :show-file-list="false"
               :before-upload="handleUpload">
               音频上传
            </el-upload>
         </div>
      </div>
      <div class="video-table">
         <div class="table-header">
            <div class="name">音频名称</div>
            <div class="operate">操作</div>
         </div>
         <div class="table-body" v-for="item in tableList">
            <div class="name">{{ item.name }}</div>
            <div class="operate"><span class="btn" @click="playVoice(item)">播放</span></div>
         </div>
      </div>
      <div class="pagination">
         <el-pagination
            background
            :page-sizes="[10, 20, 30, 50]"
            v-model:current-page="searchParams.page"
            v-model:page-size="searchParams.page_size"
            layout=" prev, pager, next"
            :total="total"
            @change="pageChange"
         />
    </div>
   </div>
</template>
<script setup>
import EventBus from '@/event-bus'
import { callPhotoAndVideoCmd, cameraParamsChangeApi, ptzResetModeApi } from '@/api/payload'
import { ElMessage } from 'element-plus'
import {
   callPhotoAndVideoCmd,
   cameraParamsChangeApi,
   ptzResetModeApi,
   startVoice,
   stayAwayRiver,
   getVoiceFile,
   playAudio,
   uploadSpeak } from '@/api/payload'
import { ElMessage, ElMessageBox } from 'element-plus'
import { throttle } from 'lodash'
import Recorder from 'js-audio-recorder';
import dayjs from 'dayjs'
const wsInfo = inject('wsInfo')
// 初始化喊话
let globalShout = null
const isRecording = ref(false)
const list1 = ref([
   { name: '广角', key: 'wide' },
   { name: '变焦', key: 'zoom' },
   { name: '红外', key: 'ir' },
])
const hasIr = inject('hasIr')
const list1 = computed(() => {
   return [
      { name: '广角', key: 'wide',show:true },
      { name: '变焦', key: 'zoom', show: true },
      { name: '红外', key: 'ir',show: hasIr.value },
   ]
})
const cameraParams = ref({
   zoom_factor: 0,
   camera_type: 'wide',
@@ -83,7 +146,7 @@
// 修改相机参数
function cameraParamsChange() {
   const { camera_type, zoom_factor} = cameraParams.value
   const { camera_type, zoom_factor } = cameraParams.value
   cameraParamsChangeApi({
      sn: dockSn.value,
      ...cameraParams.value,
@@ -125,11 +188,156 @@
// 拍照
function takePictures() {
   if (isRecording.value) {
      return ElMessage.warning('请先结束录像')
   }
   // photo拍照,video_start开始录像,video_stop结束录像
   callPhotoAndVideoCmd(dockSn.value, 'photo').then(res => {
      ElMessage.success('拍照成功')
   })
}
// 记录喊话是否开始阶段
const isRecordShouting = ref(true);
const isFlag = ref(false);
const percent = ref(0); // 滚动条滚动百分比
// 初始化喊话
function initShout() {
   destroyShout();
   globalShout = new Recorder({
      sampleBits: 16,
      sampleRate: 16000,
      numChannels: 1,
   })
}
// 销毁喊话
function destroyShout() {
   if (globalShout) {
      globalShout.pause()
      globalShout.destroy() // 销毁实例
      globalShout = null
   }
}
// 喊话
function shoutFun() {
   // 是否需要判断有控制权限
   if (isFlag.value) return
   isFlag.value = true;
   if (isRecordShouting.value) {
      // 初始化喊话
      initShout();
      globalShout.start().then(() => {
         ElMessage.success('开始喊话')
         isRecordShouting.value = false
         isFlag.value = false
      }).catch((err) => {
         isFlag.value = false
      })
   } else {
      let formData = new FormData();
      const wavBlob = globalShout.getWAVBlob()
      const wavform = new File([wavBlob], new Date().getTime() + '.wav')
      formData.append('file', wavform)
      formData.append('sn', '8UUXN2A00A00MM')
      formData.append('psdk_index', 2)
      formData.append('name', '喊话' + dayjs().format('YYYY.MM.DD HH:mm:ss'))
      formData.append('volumn', 100)
      startVoice(formData).then(res => {
         ElMessage.success('喊话成功');
         isRecordShouting.value = true
      isFlag.value = false
         destroyShout();
      }).catch(err => {
         isRecordShouting.value = true
      isFlag.value = false
         ElMessage.error('喊话失败')
         destroyShout();
      });
   }
}
const tableList = ref([]);
// 分页相关
const searchParams = ref({
   sn: droneSn.value,
  name: '',
  page: 1,
  page_size: 1
});
const isBroadcast = ref(false);
const total = ref(0);
// 广播
function broadcastFun() {
   // 是否需要判断有控制权限
   // 弹出框选择文件
   isBroadcast.value = true;
   // 获取文件接口
   getFileList();
}
// 获取音频文件列表
function getFileList() {
   getVoiceFile(searchParams.value).then((res) => {
      if (res.data.code !== 0) return ElMessage.error(res.data.msg);
      tableList.value = res.data.data.records;
      total.value = res.data.data.total;
   });
}
// 清除数据
function clearData() {
   searchParams.value.name = '';
   searchParams.value.page = 1;
   getFileList();
}
function pageChange(val) {
   searchParams.value.page = val;
   getFileList();
}
// 上传
function handleUpload(file) {
   let formData = new FormData();
  formData.append('file', file)
  formData.append('sn', droneSn.value)
   uploadSpeak(formData).then(res => {
      if (res.data.code !== 0) return ElMessage.error('上传失败');
      ElMessage.success('上传成功');
      // 获取文件接口
      getFileList();
   }).catch(err => {
      ElMessage.error('上传失败')
   })
}
// 播放操作
function playVoice(item) {
   ElMessageBox.confirm('是否确认播放该音频?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
      const { sn, name, url, md5,voice } = item
      playAudio({sn, name, url, md5,voice}).then((res) => {
         if (res.code !== 0) return ElMessage.error("播放失败");
         ElMessage.success("播放成功");
      });
   }).catch(() => {
      ElMessage({
         type: 'info',
         message: '已取消'
      });
   });
}
onMounted(() => {
   // 初始化喊话
   initShout();
});
onUnmounted(() => {
   // 销毁喊话
   destroyShout();
});
</script>
<style scoped lang="scss">
@@ -140,7 +348,7 @@
   top: 50%;
   transform: translateY(-60%);
   width: 178px;
   height: 409px;
   padding: 20px 0;
   background: rgba(0, 0, 0, 0.5);
   backdrop-filter: blur(5px);
   border-radius: 20px 20px 20px 20px;
@@ -158,16 +366,27 @@
   .cameraZoom {
      position: absolute;
      padding: 16px 0;
      padding: 20px 0 15px 0;
      left: 1600px;
      top: -59px;
      width: 112px;
      width: 90px;
      height: 490px;
      background: rgba(64, 64, 64, 0.15);
      background: rgba(0, 0, 0, 0.4);
      backdrop-filter: blur(5rem);
      border-radius: 20px 20px 20px 20px;
      display: flex;
      flex-direction: column;
      align-items: center;
      .el-slider {
         height: 100%;
         flex: 1;
      }
      .cameraZoomText{
         font-family: Segoe UI, Segoe UI;
         font-weight: 400;
         font-size: 22px;
         color: #ffffff;
         margin-top: 20px;
      }
   }
@@ -242,5 +461,166 @@
         }
      }
   }
   .percent {
      width: 146px;
      :deep(.el-progress-bar__outer) {
         height: 6px;
         background-color: rgba(255, 255, 255, 0.1);
      }
      :deep(.el-progress-bar__inner) {
         height: 6px;
         background: #FFFFFF;
         border-radius: 10px 10px 10px 10px;
         transition: all 0.3s;
      }
      :deep(.el-progress-bar__innerText) {
         // color: red;
      }
   }
}
.broadcast {
   z-index: 2;
   position: absolute;
   left: 204px;
   top: 60%;
   transform: translateY(-60%);
   backdrop-filter: blur(5px);
   width: 608px;
   height: 256px;
   // background: rgba(64,64,64,0.25);
   background: rgba(0, 0, 0, 0.5);
   border-radius: 20px 20px 20px 20px;
   .title {
      margin: 18px 8px;
      span {
         width: 66px;
         height: 18px;
         font-family: Segoe UI, Segoe UI;
         font-weight: 400;
         font-size: 14px;
         color: #EDEDED;
         line-height: 18px;
         text-align: left;
         font-style: normal;
         text-transform: none;
         margin-left: 16px;
      }
      img {
         width: 584px;
         height: 14px;
      }
      .close {
         width: 16px;
         height: 16px;
         text-align: right;
         position: absolute;
         right: 16px;
         top: 16px;
         cursor: pointer;
      }
   }
   .search-btn {
    display: flex;
      justify-content: space-between;
      color: #EDEDED;
      margin: 0 14px;
      .input-set {
         display: flex;
         align-items: center;
         width: 214px;
       height: 32px;
         label {width: 90px;}
         :deep(.el-input__wrapper) {
            background: rgba(58,58,58,0.75);
            border-radius: 8px 8px 8px 8px;
         }
         :deep(.el-input__inner) {
            color: #EDEDED;
         }
      }
      .btn-set {
         display: flex;
         gap: 12px;
         .btn {
            width: 66px;
            height: 32px;
            line-height: 32px;
            text-align: center;
            background: rgba(58,58,58,0.75);
            box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
            border-radius: 8px 8px 8px 8px;
            color: #EDEDED;
            cursor: pointer;
         }
      }
  }
   .video-table {
      margin: 14px 10px;
      .table-header {
         height: 44px;
         line-height: 44px;
         background: rgba(66,66,66,0.85);
         border-radius: 8px 8px 0px 0px;
         display: flex;
         color: #BDBDBD;
      }
      .table-body {
         height: 44px;
         line-height: 44px;
         background: rgba(74,72,72,0.6);
         border-radius: 0px 0px 8px 8px;
         display: flex;
         color: #EDEDED;
      }
      .name {
         width: 60%;
         text-align: center
      }
      .operate {
         width:40%;
         text-align: center;
         .btn {
            display: inline-block;
            cursor: pointer;
            width: 66px;
            height: 32px;
            line-height: 32px;
            text-align: center;
            background: rgba(58,58,58,0.75);
            box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
            border-radius: 8px 8px 8px 8px;
            color: #EDEDED;
         }
      }
   }
   .pagination {
    position: absolute;
      right: 10px;
    bottom: 10px;
      height: 32px;
    display: flex;
      :deep(.number) {
         color: #EDEDED;
      }
      :deep(.btn-prev),
    :deep(.btn-next) {
         width: 26px;
         height: 26px;
         background: rgba(58,58,58,0.75);
         box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
         border-radius: 8px 8px 8px 8px;
         cursor: pointer;
      }
      :deep(.el-pager li) {
         cursor: pointer;
         background: rgba(58,58,58,0.75);
         box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
         border-radius: 8px 8px 8px 8px;
         border: 1px solid rgba(255,255,255,0.99);
         margin: 0 4px;
      }
  }
}
</style>