吉安感知网项目-前端
罗广辉
2026-06-06 468e1ac46859078e74838ac2e4efebb879769f97
feat: ai图片预览
4 files modified
259 ■■■■ changed files
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue 96 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 64 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/outcomeData.vue 25 ●●●●● patch | view | raw | blame | history
packages/utils/common/index.js 74 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue
@@ -5,18 +5,11 @@
                <el-table-column label="线索缩略图" width="120">
                    <template v-slot="{ row }">
                        <el-image
                            v-if="row.attachmentType === 1 && row.resultUrl"
                            :src="row.resultUrl"
                            :preview-src-list="[row.resultUrl]"
                            v-if="row.attachmentType === 1 || row.attachmentType === 2"
                            :src="row.attachmentType === 1 ? row.resultUrl : getAiImg(row.resultUrl)"
                            :preview-src-list="[row.attachmentType === 1 ? row.resultUrl : getAiImg(row.resultUrl)]"
                            fit="cover"
                            style="width: 80px; height: 80px; border-radius: 4px"
                            preview-teleported
                        />
                        <el-image
                            v-if="row.attachmentType === 2 && row.resultUrl"
                            :src="row.resultUrl"
                            :preview-src-list="[row.resultUrl]"
                            fit="cover"
                            style="width: 80px; height: 80px"
                            preview-teleported
                        />
                    </template>
@@ -67,6 +60,7 @@
import { ref } from 'vue'
import { gdTaskResultListApi } from './achievementApi'
import DistributeDiaLog from './DistributeDiaLog.vue'
import { getAiImg } from '@ztzf/utils'
const store = useStore()
const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
@@ -111,89 +105,9 @@
    try {
        const res = await gdTaskResultListApi({ patrolTaskId: currentRow.value.id })
        list.value = res?.data?.data ?? []
        list.value = await Promise.all(
            list.value.map(async i => {
                const aiImg = await getAiImg(i.resultUrl)
                return { ...i, aiImg }
            })
        )
    } finally {
        loading.value = false
    }
}
const aiFrame = [
    '{"score":0.91357421875,"bbox":{"x_cen":1246.0,"y_cen":209.0,"width":166.0,"height":334.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
    '{"score":0.89697265625,"bbox":{"x_cen":370.0,"y_cen":694.5,"width":162.0,"height":331.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
    '{"score":0.89501953125,"bbox":{"x_cen":396.0,"y_cen":343.0,"width":168.0,"height":330.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
    '{"score":0.79296875,"bbox":{"x_cen":409.5,"y_cen":52.5,"width":167.0,"height":105.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
]
function getAiImg(url) {
    if (!url) return ''
    const img = new Image()
    img.crossOrigin = 'anonymous'
    return new Promise(resolve => {
        img.onload = () => {
            if (!img.naturalWidth || !img.naturalHeight) {
                resolve('')
                return
            }
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            if (!ctx) {
                resolve('')
                return
            }
            canvas.width = img.naturalWidth
            canvas.height = img.naturalHeight
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
            aiFrame.forEach(item => {
                let target = item
                try {
                    target = typeof item === 'string' ? JSON.parse(item) : item
                } catch (error) {
                    return
                }
                const { x_cen, y_cen, width, height } = target.bbox || {}
                if ([x_cen, y_cen, width, height].some(value => typeof value !== 'number')) return
                const x = x_cen - width / 2
                const y = y_cen - height / 2
                const label = target.class_name || ''
                const fontSize = Math.max(18, Math.round(canvas.width / 80))
                const labelHeight = fontSize + 10
                const labelY = y - labelHeight >= 0 ? y - labelHeight : y
                ctx.strokeStyle = '#FF3B30'
                ctx.lineWidth = Math.max(3, Math.round(canvas.width / 640))
                ctx.strokeRect(x, y, width, height)
                if (label) {
                    ctx.font = `${fontSize}px Arial`
                    const labelWidth = ctx.measureText(label).width + 16
                    ctx.fillStyle = '#FF3B30'
                    ctx.fillRect(x, labelY, labelWidth, labelHeight)
                    ctx.fillStyle = '#FFFFFF'
                    ctx.textBaseline = 'middle'
                    ctx.fillText(label, x + 8, labelY + labelHeight / 2)
                }
            })
            try {
                resolve(canvas.toDataURL('image/jpeg', 0.92))
            } catch (error) {
                console.log(error)
                resolve('')
            }
        }
        img.onerror = () => resolve('')
        img.src = url
    })
}
// 打开分发弹框
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -82,19 +82,15 @@
                            任务成果({{ taskResultList.length || 0 }}条)
                        </div>
                        <div class="imgBox">
                            <div v-for="item in taskResultList.filter(item => item.resultUrl && [1,2,3].includes( item.attachmentType))">
                            <div
                                v-for="item in taskResultList.filter(item1 => item1.resultUrl && [1, 2, 3].includes(item1.attachmentType))"
                            >
                                <el-image
                                    v-if="item.attachmentType === 1"
                                    :src="item.resultUrl"
                                    :preview-src-list="[item.resultUrl]"
                                    v-if="item.attachmentType === 1 || item.attachmentType === 2"
                                    :src="item.attachmentType === 1 ? item.resultUrl : getAiImg(item.resultUrl)"
                                    :preview-src-list="[item.attachmentType === 1 ? item.resultUrl : getAiImg(item.resultUrl)]"
                                    fit="cover"
                                    preview-teleported
                                />
                                <el-image
                                    v-if="item.attachmentType === 2"
                                    :src="item.resultUrl"
                                    :preview-src-list="[item.resultUrl]"
                                    fit="cover"
                                    style="width: 80px; height: 80px"
                                    preview-teleported
                                />
                                <div class="video-btn" v-if="item.attachmentType === 3" @click="videoClick(item)">
@@ -123,7 +119,7 @@
                                    popper-class="gd-select-popper"
                                    v-model="formData.patrolTaskType"
                                    :options="workOrderTypeXT"
                                    :props="{...taskTypeCascaderProps,multiple:true}"
                                    :props="{ ...taskTypeCascaderProps, multiple: true }"
                                    placeholder="请选择"
                                    collapse-tags
                                    clearable
@@ -282,21 +278,24 @@
        v-if="VideoShow"
        v-model="VideoShow"
        :playUrl="currentVideo.resultUrl"
    >
    </VideoPlayDialog>
    ></VideoPlayDialog>
</template>
<script setup>
import { computed, ref, onMounted, inject } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldRules, flyVisual, getDictLabel, geomAnalysis } from '@ztzf/utils'
import { fieldRules, flyVisual, geomAnalysis, getAiImg } from '@ztzf/utils'
import {
    gdPatrolTaskRepublish,
    gdFlyerPageApi,
    gdPatrolTaskAuditApi,
    gdPatrolTaskDetailApi,
} from './inspectionRequestApi'
import { gdWorkOrderFlowListApi, gdWorkOrderFlowPatrolListApi, gdWorkOrderDetailApi } from '../orderManage/orderManageApi'
import {
    gdWorkOrderFlowListApi,
    gdWorkOrderFlowPatrolListApi,
    gdWorkOrderDetailApi,
} from '../orderManage/orderManageApi'
import { gdManageDeviceListApi } from '../orderManage/gdManageDeviceApi'
import { pxToRem } from '@/utils/rem'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
@@ -363,7 +362,7 @@
            children: (group.algorithms || []).map(alg => ({
                id: alg.id,
                name: alg.name,
            }))
            })),
        }))
    } catch (e) {
        console.error('获取算法列表失败', e)
@@ -374,10 +373,12 @@
function getAlgorithmNames(algorithmIds) {
    if (!algorithmIds || !algorithmIds.length) return ''
    const allAlgorithms = algorithmTreeData.value.flatMap(group => group.children || [])
    return algorithmIds.map(id => {
        const item = allAlgorithms.find(alg => alg.id === id)
        return item ? item.name : id
    }).join(', ')
    return algorithmIds
        .map(id => {
            const item = allAlgorithms.find(alg => alg.id === id)
            return item ? item.name : id
        })
        .join(', ')
}
const gdStatusObj = {
@@ -531,18 +532,18 @@
function getAirDetails() {
    const dockHeight = formData.value.height
    queryAirById(formData.value.patrolRouteUrl).then(res => {
        const { airlineWaypoints, airlineSetting } = res.data.data;
        const { airlineWaypoints, airlineSetting } = res.data.data
        // 使用空值合并运算符 ?? 或 逻辑或 || 兜底
        const { globalAirlineHeight = 0, airlineHeightMode } = airlineSetting || {};
        const { globalAirlineHeight = 0, airlineHeightMode } = airlineSetting || {}
        const relative = airlineHeightMode === 'relativeToStartPoint'
        const list = airlineWaypoints
        if (!list.length) return mapRef.value?.flyBoundary()
        const result = list.map(item => {
            let height = item?.globalHeight || globalAirlineHeight
            if (relative){
            if (relative) {
                height = height + Number(dockHeight)
            }
            return [Number(item.longitude), Number(item.latitude),height || 0]
            return [Number(item.longitude), Number(item.latitude), height || 0]
        })
        viewer.entities.add({
            polyline: {
@@ -599,7 +600,7 @@
    getAlgorithmList()
    // 获取工单详情并根据geom范围获取航线列表
    await getWorkOrderDetail(formData.value.workOrderId)
    ;['6', '7', '8'].includes(row.taskStatus) && await getTaskResultList()
    ;['6', '7', '8'].includes(row.taskStatus) && (await getTaskResultList())
    loadList()
    initMap()
    getAirDetails()
@@ -680,9 +681,7 @@
        transition: box-shadow 0.16s ease;
        &:hover {
            box-shadow:
                inset 0 0 0 2px rgba(76, 52, 255, 0.48),
                0 8px 20px rgba(76, 52, 255, 0.24);
            box-shadow: inset 0 0 0 2px rgba(76, 52, 255, 0.48), 0 8px 20px rgba(76, 52, 255, 0.24);
        }
    }
    .el-image {
@@ -696,9 +695,8 @@
        overflow: hidden;
        //border: 1px solid #D8D6FF;
        border-radius: 4px;
        background:
            linear-gradient(135deg, rgba(76, 52, 255, 0.14), rgba(76, 52, 255, 0) 48%),
            linear-gradient(180deg, #F4F5FF 0%, #E9ECFF 100%);
        background: linear-gradient(135deg, rgba(76, 52, 255, 0.14), rgba(76, 52, 255, 0) 48%),
            linear-gradient(180deg, #f4f5ff 0%, #e9ecff 100%);
        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
        display: flex;
        align-items: center;
@@ -706,7 +704,7 @@
        cursor: pointer;
        &::before {
            content: "";
            content: '';
            position: absolute;
            inset: 0;
            background-image: linear-gradient(
applications/task-work-order/src/views/orderView/orderManage/orderManage/outcomeData.vue
@@ -38,21 +38,14 @@
          <el-table-column type="index" label="序号" width="80" />
          <el-table-column label="图片/视频" >
            <template v-slot="{ row }">
              <el-image
                v-if="row.attachmentType ===1 && row.resultUrl"
                :src="row.resultUrl"
                :preview-src-list="[row.resultUrl]"
                fit="cover"
                style="width: 80px; height: 80px; border-radius: 4px;"
                                preview-teleported
              />
              <el-image
                                    v-if="row.attachmentType === 2 && row.resultUrl"
                                    :src="row.resultUrl"
                                    :preview-src-list="[row.resultUrl]"
                                    fit="cover"
                                    preview-teleported
                                />
                            <el-image
                                v-if="row.attachmentType === 1 || row.attachmentType === 2"
                    :src="row.attachmentType === 1 ? row.resultUrl : getAiImg(row.resultUrl)"
                    :preview-src-list="[row.attachmentType === 1 ? row.resultUrl : getAiImg(row.resultUrl)]"
                    fit="cover"
                    style="width: 80px; height: 80px"
                    preview-teleported
                            />
                            <div class="video-btn" v-if="row.attachmentType === 3 && row.resultUrl" @click="videoClick(row)">
                                <el-icon :size="30" color="#fff">
                                    <VideoPlay />
@@ -96,7 +89,7 @@
import {gdTaskResultPageApi, gdTaskResultDownloadApi, gdTaskResultRemoveApi,listByWorkOrderId}from './orderManageApi'
import { Search, RefreshRight, Download } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDictLabel } from '@ztzf/utils'
import { getAiImg, getDictLabel } from '@ztzf/utils'
import dayjs from 'dayjs'
import VideoPlayDialog from '@/components/VideoPlayDialog.vue'
packages/utils/common/index.js
@@ -73,3 +73,77 @@
            return { longitude, latitude }
        })
}
// 图片转带ai框的图片
export function getAiImg(url, aiFrameSource) {
    if (!url) return ''
    const aiFrame = aiFrameSource || [
        '{"score":0.91357421875,"bbox":{"x_cen":1246.0,"y_cen":209.0,"width":166.0,"height":334.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
        '{"score":0.89697265625,"bbox":{"x_cen":370.0,"y_cen":694.5,"width":162.0,"height":331.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
        '{"score":0.89501953125,"bbox":{"x_cen":396.0,"y_cen":343.0,"width":168.0,"height":330.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
        '{"score":0.79296875,"bbox":{"x_cen":409.5,"y_cen":52.5,"width":167.0,"height":105.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}',
    ]
    const img = new Image()
    img.crossOrigin = 'anonymous'
    return new Promise(resolve => {
        img.onload = () => {
            if (!img.naturalWidth || !img.naturalHeight) {
                resolve('')
                return
            }
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            if (!ctx) {
                resolve('')
                return
            }
            canvas.width = img.naturalWidth
            canvas.height = img.naturalHeight
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
            aiFrame.forEach(item => {
                let target = item
                try {
                    target = typeof item === 'string' ? JSON.parse(item) : item
                } catch (error) {
                    return
                }
                const { x_cen, y_cen, width, height } = target.bbox || {}
                if ([x_cen, y_cen, width, height].some(value => typeof value !== 'number')) return
                const x = x_cen - width / 2
                const y = y_cen - height / 2
                const label = target.class_name || ''
                const fontSize = Math.max(18, Math.round(canvas.width / 80))
                const labelHeight = fontSize + 10
                const labelY = y - labelHeight >= 0 ? y - labelHeight : y
                ctx.strokeStyle = '#FF3B30'
                ctx.lineWidth = Math.max(3, Math.round(canvas.width / 640))
                ctx.strokeRect(x, y, width, height)
                if (label) {
                    ctx.font = `${fontSize}px Arial`
                    const labelWidth = ctx.measureText(label).width + 16
                    ctx.fillStyle = '#FF3B30'
                    ctx.fillRect(x, labelY, labelWidth, labelHeight)
                    ctx.fillStyle = '#FFFFFF'
                    ctx.textBaseline = 'middle'
                    ctx.fillText(label, x + 8, labelY + labelHeight / 2)
                }
            })
            try {
                resolve(canvas.toDataURL('image/jpeg', 0.92))
            } catch (error) {
                console.log(error)
                resolve('')
            }
        }
        img.onerror = () => resolve('')
        img.src = url
    })
}