无人机管理后台前端(已迁走)
张含笑
2025-08-13 73e5d0d8bd018cde09c541988d82e6bbb880f133
feat:成果数据展示所有数据
4 files modified
383 ■■■■ changed files
src/api/job/task.js 7 ●●●●● patch | view | raw | blame | history
src/views/job/components/DeviceJobDetails.vue 358 ●●●● patch | view | raw | blame | history
src/views/job/components/DeviceJobDetailsMap.vue 14 ●●●● patch | view | raw | blame | history
src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue 4 ●●● patch | view | raw | blame | history
src/api/job/task.js
@@ -239,3 +239,10 @@
        },
    })
}
export const getJobsAllFiles = data => {
    return request({
        url: `/blade-resource/attach/aiImagesPage?current=1&size=9999`,
        method: 'post',
        data: data,
    })
}
src/views/job/components/DeviceJobDetails.vue
@@ -11,7 +11,7 @@
                            <div class="itemBox">
                                <div class="itemTitle">{{ item.name }}:</div>
                                <div truncated class="itemValue" v-if="item.name !== '飞行事件' && item.name !== '任务执行次数'">{{
        item.value ? item.value : '--' }}</div>
                                        item.value ? item.value : '--' }}</div>
                                <div class="itemValue" v-if="item.name === '任务执行次数'">{{ item.value ? item.value : '0'
                                    }}次</div>
                                <!-- 飞行事件 -->
@@ -34,49 +34,133 @@
                        <span>{{ total }}</span>
                        个
                    </p>
                    <p class="more" @click="jumpMore">更多</p>
                    <div class="rightBox">
                        <div class="downloadBtn" @click="htlsrwxq === 100 && downloadFun()">
                            <el-progress v-if="htlsrwxq !== 100" :percentage="htlsrwxq" :show-text="false" striped striped-flow :duration="1" />
                            <div class="downloadBtnText">
                                <span v-if="htlsrwxq === 100">下载</span>
                                <template v-else>
                                    处理中<el-icon @click="cancelDownload"><CircleClose /></el-icon>
                                </template>
                            </div>
                        </div>
                        <div class="downloadBtn" @click="showAll = !showAll" v-if="total > 5">
                            {{ showAll ? '收起' : '更多' }}
                        </div>
                    </div>
                </div>
                <div class="imgListBox">
                    <!-- 图片显示 -->
                    <!-- :preview-src-list="achievementList.map(i => i.url)" -->
                    <template v-for="(item, index) in achievementList" :key="index">
                        <el-image v-if="item.result_type === 0" class="imgItem" :src="getSmallImg(item.url)"
                            :preview-src-list="achievementList.map(i => i.url)" show-progress preview-teleported
                            :initial-index="index" fit="cover" />
                        <div v-if="item.result_type === 1" class="videotime">
                            <video ref="videoRef" class="videoDisplay" :src="item.url"></video>
                            <img @click="enterFullScreen(index)" class="videobutton"
                                src="@/assets/images/task/videoshow.png" alt="">
                        </div>
                        <el-dialog class="ztzf-dialog " append-to-body modal-class="detailsOfHistoricalTasks"
                            v-model="VideoShow" width="1600" :close-on-click-modal="false"
                            :destroy-on-close="true">
                            <div class="fullscreen">
                                <video ref="fullscreenVideo" class="fullscreen-video" :src="currentVideoUrl"
                                    :style="{ width: 1600, height: '80vh' }" controls autoplay></video>
                    <template v-for="(item, index) in achievementList.slice(0, visibleCount)" :key="index">
                    <div class="result-item">
                        <el-checkbox v-model="item.checked" />
                        <div class="itemName">{{ item.createTime }}</div>
                        <!-- 正射 -->
                            <el-image
                                v-if="item.resultType === 4"
                                :src="item.smallUrl"
                                :preview-src-list="[item.showUrl]"
                                fit="cover"
                                preview-teleported
                            />
                            <!-- 全景 -->
                            <img
                                v-else-if="item.resultType === 5"
                                class="quanjing"
                                @click="clickpanorama(item)"
                                :src="item?.smallUrl"
                                alt=""
                            />
                        <!-- 视频 -->
                            <div v-else-if="item.resultType === 1" class="videotime">
                                <img
                                    class="videoDisplay"
                                    :src="convertVideoUrlToThumbnail(item.link)"
                                    alt=""
                                    @click="enterFullScreen(index)"
                                />
                                <img
                                    @click="enterFullScreen(index)"
                                    class="videobutton"
                                    src="@/assets/images/task/videoshow.png"
                                    alt=""
                                />
                            </div>
                        </el-dialog>
                                <!-- 图片 -->
                            <el-image
                                v-else
                                :src="item.smallUrl"
                                :preview-src-list="[item.showUrl]"
                                preview-teleported
                                :initial-index="index"
                                fit="cover"
                            />
                        <el-dialog
              class="ztzf-dialog"
              append-to-body
              modal-class="detailsOfHistoricalTasks"
              v-model="VideoShow"
              :width="pxToRem(1600)"
              :close-on-click-modal="false"
              :destroy-on-close="true"
            >
                <div class="fullscreen">
                    <video
                        ref="fullscreenVideo"
                        class="fullscreen-video"
                        :src="currentVideoUrl"
                        :style="{ width: pxToRem(1600), height: '80vh' }"
                        controls
                        autoplay
                    ></video>
                </div>
            </el-dialog>
                        </div>
                    </template>
                </div>
            </div>
            <DeviceJobDetailsMap v-if="isShow" class="contentRight" :detailsData="detailsData" />
            <DeviceJobDetailsMap v-if="isShow" :yuanImages="yuanImages" class="contentRight" :detailsData="detailsData" />
        </div>
    </el-dialog>
  <!-- 全景360 -->
  <PanoramaPopup
    v-if="'全景'"
    v-model:panoramaParamsShow="panoramaParamsShow"
    v-model:panoramaParamsUrl="panoramaParamsUrl"
  />
</template>
<script setup>
import PanoramaPopup from '@/components/PanoramaPopup/PanoramaPopup.vue';
import { getShowImg, getSmallImg, getzsSmallImg,getzsShowImg } from '@/utils/util';
import { getaiImagesPageAPI,cancelDownloadApi,getDownloadStatusApi,attachDownload} from '@/api/dataCenter/dataCenter';
import { pxToRem } from '@/utils/rem'
import JobRelatedEvents from './JobRelatedEvents.vue'
import { getEventMediaListApi, getJobDetails, getJobInfoFiles } from '@/api/job/task'
import { getEventMediaListApi, getJobDetails, getJobInfoFiles,getJobsAllFiles } from '@/api/job/task'
import DeviceJobDetailsMap from './DeviceJobDetailsMap.vue'
import { droneEventList } from '../const/drc'
import { ElMessage } from 'element-plus'
import { ElMessage,ElLoading } from 'element-plus'
import { useStore } from 'vuex';
const store = useStore();
const htlsrwxq = computed(() => store.state.common.downloadProgress?.htlsrwxq || 100)
const isShow = defineModel('show')
const VideoShow = ref(false)
const showAll = ref(false) // 是否展示全部
let loadingData
// 原图
const yuanImages = ref([])
function convertVideoUrlToThumbnail(videoUrl) {
    // 检查是否是有效的视频URL
    if (!videoUrl || typeof videoUrl !== 'string') {
        return videoUrl
    }
    // 替换文件扩展名
    return videoUrl.replace(/\.mp4$/, '_small.jpg')
}
const infoList = ref([
    { name: '任务编号', value: '', field: 'job_info_num' },
    { name: '任务名称', value: '', field: 'name' },
@@ -110,26 +194,40 @@
// 任务成果
const total = ref(0)
const achievementList = ref([])
const props = defineProps(['wayLineJodInfoId', 'waylineJobId', 'batchNo'])
const props = defineProps(['wayLineJodInfoId', 'waylineJobId', 'batchNo','jobId'])
const wayLineJodInfoId = computed(() => props.wayLineJodInfoId)
provide('wayLineJodInfoId', wayLineJodInfoId)
// 可见数量
const visibleCount = computed(() =>
    showAll.value ? achievementList.value.length : Math.min(5, achievementList.value.length)
)
const getAchievement = () => {
    if (!props.wayLineJodInfoId) return
    const params = {
        "way_line_jod_info_id": props.wayLineJodInfoId,
        "batch_no": props.batchNo
    if (!props.jobId) return
    const attachmentsParams = {
        'wayLineJobId': props.jobId,
        'resultTypes': [0, 1, 3, 4, 5],
        'orderByCreateTime': true,
    }
    getEventMediaListApi(params).then(res => {
        achievementList.value = res.data.data.slice(0, 5)
        total.value = res.data.data.length
    const pageParams = {
        current: 1,
        size: 2000,
    }
        getaiImagesPageAPI(attachmentsParams,pageParams ).then(res => {
        achievementList.value = res.data.data.records.map(i => ({
            ...i,
            checked: false,
            smallUrl: i.resultType === 4 ? getzsSmallImg(i.link) : getSmallImg(i.link),
            showUrl: i.resultType === 4 ? getzsShowImg(i.link) : getShowImg(i.link),
        }))
        total.value = res.data.data.total
        console.log('成果数据', res.data.data)
    })
}
const jumpMore = () => {
    ElMessage.warning('正在加急开发中...')
}
const getSmallImg = url => {
    return url ? url.substring(0, url.lastIndexOf('.')) + '_small' + url.substring(url.lastIndexOf('.')) : ''
}
const flightEvents = ref([])
const jobTimes = ref([])
const getDetails = () => {
@@ -141,7 +239,6 @@
    getJobDetails(params).then(res => {
        detailsData.value = res.data.data
        jobTimes.value = res.data.data.job_times
        infoList.value.forEach(item => {
            if (item.name === '任务频次') {
                const { rep_rule_type = '', rep_rule_val = '' } = detailsData?.value || {}
@@ -155,6 +252,15 @@
                    droneEventList.filter(({ value }) => actionActuatorFunc === value)
                )
            }
                getJobsAllFiles({
            wayLineJobId: detailsData.value.way_lines.map(item => item.job_id).join(','),
            resultTypes: [0, 2, 4, 5],
        }).then(result => {
            if (result.data.code !== 200) return
            yuanImages.value = result.data.data.records
            console.log('ssssss',yuanImages.value);
        })
        })
        // flystatus.value = res.data.data.ai_type_str
    })
@@ -169,7 +275,7 @@
}
// 计算当前视频URL
const currentVideoUrl = computed(() => {
    return achievementList.value[currentVideoIndex.value]?.url || ''
    return achievementList.value[currentVideoIndex.value]?.link || ''
})
// 关闭弹窗处理
const handleClose = () => {
@@ -178,7 +284,41 @@
        fullscreenVideo.value.currentTime = 0
    }
}
// 下载
const downloadFun = async () => {
    const list = achievementList.value.filter(i => i.checked)
    console.log('list',list);
    if (!list?.length) return ElMessage.warning('请选择文件')
    if (list.length === 1) {
        list.forEach((item, index) => {
            const suffix = item.url.split('.').pop()
            aLinkDownloadUtil(item.url, item.nickName + '.' + suffix)
        })
    } else {
        loadingData = ElLoading.service({ background: 'rgba(0, 0, 0, 0.5)', text: '打包中,请稍等...' })
        const fileIds = list.map(i => i.id)
        const res = await getDownloadStatusApi({type: 'htlsrwxq'})
        if (!['CANCELLED','COMPLETED'].includes(res.data.data?.status || 'COMPLETED')){
            return ElMessage.warning('还有正在处理的')
        }
        attachDownload({ attachIds: fileIds,type:'htlsrwxq' }).finally(()=>{
            loadingData.close()
        })
    }
}
// 全景预览
const panoramaParamsShow = ref(false)
const panoramaParamsUrl = ref(null)
const clickpanorama = val => {
    panoramaParamsShow.value = true
    panoramaParamsUrl.value = val.link
}
// 取消下载
function cancelDownload() {
    cancelDownloadApi({ type:'htlsrwxq' }).then(res =>{
        ElMessage.success('取消成功')
    })
}
onMounted(() => {
    getDetails()
    getAchievement()
@@ -235,7 +375,52 @@
            display: flex;
            justify-content: space-between;
            align-content: center;
.rightBox{
                display: flex;
                align-items: center;
                padding-right: 10px;
                gap: 0 10px;
                .downloadBtn{
                    position: relative;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    height: 28px;
                    padding: 0 15px;
                    background: rgba(28, 92, 255, 0.14);
                    border-radius: 4px 4px 4px 4px;
                    border: 1px solid #1c5cff;
                    cursor: pointer;
                        color: #1C5CFF;
                    .downloadBtnText{
                        display: flex;
                        align-items: center;
                        gap: 5px;
                        z-index: 5;
                    }
                    .el-progress{
                        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;
                                }
                            }
                        }
                    }
                }
            }
            p {
                display: inline-block;
                margin-left: 30px;
@@ -333,8 +518,92 @@
            grid-template-columns: repeat(5, 1fr);
            gap: 10px;
            margin-bottom: 49px;
            > * {
                width: 100%;
                height: 200px;
                position: relative;
                border-radius: 0px 0px 4px 4px;
                overflow: hidden;
            }
            :deep(.el-checkbox__inner) {
                width: 17px !important;
                height: 17px !important;
                background: rgba(37, 36, 36, 0.63) !important;
                border-radius: 4px 4px 4px 4px;
                border: 1px solid #ffffff !important;
            }
            .el-checkbox {
                position: absolute;
                top: 0;
                left: 6px;
            }
            :deep(.el-checkbox__inner:after) {
                position: absolute;
                left: 50% !important;
                top: 40% !important;
                transform: translate(-50%, -50%) rotate(45deg) !important;
            }
            :deep(.el-checkbox.is-checked .el-checkbox__inner) {
                background-color: #09297b !important;
                border-color: #1c5cff !important;
            }
            .el-image {
                width: 100%;
                height: 100%;
                cursor: pointer;
                position: relative;
            }
            .quanjing {
                width: 100%;
                height: 100%;
                object-fit: cover;
                cursor: pointer;
                position: relative;
            }
            .videotime {
                width: 100%;
                height: 100%;
                position: relative;
                .videoDisplay {
                    width: 100%;
                    height: 100%;
                    object-fit: cover;
                }
            }
            .itemName {
                position: absolute;
                bottom: 0;
                left: 0;
                height: 23px;
                width: 100%;
                background: rgba(22, 22, 22, 0.68);
                border-radius: 0px 0px 4px 4px;
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: 400;
                font-size: 14px;
                color: #ffffff;
                line-height: 23px;
                z-index: 9;
                padding: 0 6px;
            }
        }
.videobutton {
            position: absolute;
            top: 40%;
            left: 50%;
            transform: translate(-50%);
            cursor: pointer;
            width: 40px;
            height: 40px;
            display: inline-block;
            background: #999;
            border-radius: 50%;
        }
        .videoDisplay {
            display: flex;
            flex-wrap: wrap;
@@ -346,17 +615,6 @@
        .videotime {
            position: relative;
        }
        // .videobutton {
        //     position: absolute;
        //     top: 40%;
        //     left: 45%;
        //     transform: translate(-50%);
        //     cursor: pointer;
        //     width: 40px;
        //     height: 40px;
        //     display: inline-block;
        // }
    }
    .contentRight {
src/views/job/components/DeviceJobDetailsMap.vue
@@ -1,13 +1,3 @@
<!--
 * @Author       : yuan
 * @Date         : 2025-05-26 17:30:33
 * @LastEditors  : yuan
 * @LastEditTime : 2025-06-10 15:46:18
 * @FilePath     : \src\views\job\components\DeviceJobDetailsMap.vue
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-05-26 17:30:33
-->
<template>
    <div id="deviceJobDetailsMap" class="ztzf-cesium">
        <PlanarRouteLineList :curRouteLineData="curRouteLineData" @routeLineListClick="routeLineListClick" />
@@ -18,8 +8,8 @@
import { PublicCesium } from '@/utils/cesium/publicCesium'
const props = defineProps(['detailsData'])
const props = defineProps(['detailsData','yuanImages'])
console.log('props.yua',props.yuanImages);
import { useRouteLine } from '@/hooks/useRouteLine/useRouteLine.js'
// 加载航线hook
src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue
@@ -104,7 +104,7 @@
    /> -->
    <!-- 历史任务详情 -->
    <DeviceJobDetails v-if="isShowDeviceJobDetails" v-model:show="isShowDeviceJobDetails" :wayLineJodInfoId="rowData.id"
        :batchNo="rowData.batch_no" />
        :batchNo="rowData.batch_no" :jobId="jobId"/>
    <CancelTaskDialog ref="cancelTaskDialogRef" v-model:isShowCancelTask="isShowCancelTask" :row-data="rowData"
        @refresh="getJobList" />
</template>
@@ -166,10 +166,12 @@
// 查看当前任务详情 如果是一台机则显示详情 如果是多台机则进入集群调度(暂未开发)
let rowData = ref({})
const jobId = ref('')
const handleDetail = row => {
    if (!row.device_sns.length) return ElMessage.warning('没有device_sns')
    if (row.device_sns.length !== 1) return ElMessage.success('即将跳转到集群调度')
    rowData.value = row ? row : {}
jobId.value = rowData.value?.job_id
    if (row.status === 2 || row.status === 1) {
        // isShowCurrentTaskDetails.value = true
        // 跳转大屏当前任务详情