forked from drone/command-center-dashboard

chenyao
2025-04-18 0656a64931e0517f8791f983f1a872047cc4a27b
feat: 增加广播和播放功能
6 files modified
2 files added
438 ■■■■■ changed files
src/api/payload.js 52 ●●●●● patch | view | raw | blame | history
src/assets/images/taskManagement/taskIntermediateContent/broadcast-list.png patch | view | raw | blame | history
src/assets/images/taskManagement/taskIntermediateContent/close.png patch | view | raw | blame | history
src/components/CurrentTaskDetails/CurrentTaskDetails.vue 5 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsLeft.vue 375 ●●●●● patch | view | raw | blame | history
src/views/SignMachineNest/SignMachineNest.vue 2 ●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/TaskIntermediateContent.vue 2 ●●● patch | view | raw | blame | history
src/views/TaskManage/TaskTop/TaskIndustry.vue 2 ●●● patch | view | raw | blame | history
src/api/payload.js
@@ -79,5 +79,57 @@
  });
};
// 喊话
export const startVoice = (data) => {
  return request({
    url:`/drone-device-core/speak/api/v1/startVoice`,
    method: 'post',
    data,
  });
};
//播报列表单个音频文件播放(远离河道)
export const stayAwayRiver = (params) => {
  const url = `/drone-device-core/speak/api/v1/putVoice`;
  return request({
      url,
      method: 'post',
      params,
  });
};
//播报文件
export const getVoiceFile = (params) => {
  const url = `/drone-device-core/speak/api/v1/getVoiceFile`;
  return request({
      url,
      method: 'post',
      params,
  });
};
// 上传音频文件
export const uploadSpeak = (data) => {
  const url = `/drone-device-core/speak/api/v1/uploadSpeak`;
  return request({
      url,
      method: 'post',
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      data,
  });
};
// 播放音频
export const playAudio = (params) => {
  const url = `/drone-device-core/speak/api/v1/startVoices`;
  return request({
      url,
      method: 'post',
      params,
  });
};
src/assets/images/taskManagement/taskIntermediateContent/broadcast-list.png
src/assets/images/taskManagement/taskIntermediateContent/close.png
src/components/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -17,9 +17,10 @@
            <RealTimeMap :class="`${isMaxMap ? 'maxBox' : 'minBox'}`" />
            <TaskDetailsRight v-if="isAutoControl" />
            <template v-else>
                <TaskDetailsHead />
                <TaskDetailsLeft />
            </template>
            <TaskDetailsHead />
                <TaskDetailsLeft />
            <!--    控制面板,里面有方法需要立即执行,不可用v-if        -->
            <ControlPanel v-show="!isAutoControl" />
src/components/CurrentTaskDetails/TaskDetailsLeft.vue
@@ -22,10 +22,12 @@
            <div @click="recordFun">{{ isRecording ? '录像中...' : '录像' }}</div>
        </div>
        <div class="multiCol">
            <div>喊话</div>
            <div>广播</div>
            <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"
@@ -36,13 +38,71 @@
            />
        </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'
// 初始化喊话
let globalShout = null
const isRecording = ref(false)
@@ -130,6 +190,148 @@
        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 +342,7 @@
    top: 50%;
    transform: translateY(-60%);
    width: 178px;
    height: 409px;
    height: 416px;
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(5px);
    border-radius: 20px 20px 20px 20px;
@@ -242,5 +444,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>
src/views/SignMachineNest/SignMachineNest.vue
@@ -39,7 +39,7 @@
            break
        }
        case EBizCode.DeviceOsd: {
            // console.log(payload, 'DeviceOsd')
            console.log(payload, '888888888')
            store.commit('setDeviceInfo', payload)
            store.commit('setWsMessage', payload)
            break
src/views/TaskManage/TaskIntermediateContent/TaskIntermediateContent.vue
@@ -98,7 +98,7 @@
};
// 查看当前任务详情 如果是一台机则显示详情 如果是多台机则进入集群调度(暂未开发)
let isShowCurrentTaskDetails = ref(false);
let isShowCurrentTaskDetails = ref(true);
let rowData = ref({});
const handleDetail = (row) => {
    if (row.device_sns.length === 1){
src/views/TaskManage/TaskTop/TaskIndustry.vue
@@ -1,7 +1,7 @@
<!-- 任务算法统计 -->
 <template>
  <div class="task-industry">
    <div class="title">任务算法类型</div>
    <div class="title">机巢事件前五数量占比</div>
    <div class="chart" ref="chartRef"></div>
  </div>
 </template>