吉安感知网项目-前端
chenyao
2026-06-30 4b3c357f2fa24a27eac3fe59cbf64e34255ab212
Merge remote-tracking branch 'origin/master'
30 files modified
1580 ■■■■ changed files
README.md 4 ●●●● patch | view | raw | blame | history
applications/drone-command/index.html 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/top/index.vue 26 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/README.md 8 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appComponents/workMap/index.vue 111 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/work/workDetail/index.vue 124 ●●●●● patch | view | raw | blame | history
applications/task-work-order/README.md 5 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/components/VideoPlayDialog.vue 120 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/deviceInvocation/deviceCallRecord/FormDiaLog.vue 6 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/flyingHand/FlyingHandDialog.vue 30 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/flyingHand/index.vue 15 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/DistributeDiaLog.vue 8 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue 174 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/index.vue 9 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/index.vue 72 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/FormDiaLog.vue 31 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 114 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue 125 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue 62 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue 95 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/outcomeData.vue 67 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/taskTypeOptions.js 52 ●●●●● patch | view | raw | blame | history
package.json 2 ●●● patch | view | raw | blame | history
packages/utils/common/index.js 66 ●●●●● patch | view | raw | blame | history
pnpm-lock.yaml 18 ●●●● patch | view | raw | blame | history
uniapps/work-app/package.json 8 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/config/env.js 4 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/manifest.json 13 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/work/index.vue 193 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/workDetail/index.vue 14 ●●●●● patch | view | raw | blame | history
README.md
@@ -14,7 +14,7 @@
## 环境要求
- Node.js >= 20
- pnpm >= 9
- pnpm >= 9.15.9
## 快速开始
```bash
@@ -44,4 +44,4 @@
- `applications/drone-app-web-view/README.md`
## 备注
- 目前未配置测试框架,`pnpm test` 会直接退出。
- 目前未配置测试框架,`pnpm test` 会直接退出。
applications/drone-command/index.html
@@ -23,11 +23,11 @@
    <!-- 高德地图api更新必须配合安全密钥使用 -->
    <script>
        window._AMapSecurityConfig = {
            securityJsCode: '1cd61831ddce188f3129b761f57024fc',
            securityJsCode: 'xxx',
        }
    </script>
    <script type="text/javascript"
                    src='https://webapi.amap.com/maps?v=2.0&key=eb0654440644f710110547d2132b2fbe&plugin=AMap.PlaceSearch,AMap.Geocoder'></script>
                    src='https://webapi.amap.com/maps?v=2.0&key=xxx&plugin=AMap.PlaceSearch,AMap.Geocoder'></script>
    <script src="https://webapi.amap.com/ui/1.1/main.js?v=1.0.11"></script>
    <title>吉安市低空经济服务一体化平台</title>
</head>
applications/drone-command/src/page/index/top/index.vue
@@ -8,8 +8,8 @@
      <div class="header-right">
        <nav class="nav-menu">
          <div v-for="(item, index) in topMenus" :key="index" class="nav-item"
            :class="{ active: item.active }" @click="handleMenuClick(item)">
          <div v-for="(item, index) in topMenus" :key="index" class="nav-item" :class="{ active: item.active }"
            @click="handleMenuClick(item)">
            <span>{{ item.label }}</span>
          </div>
        </nav>
@@ -135,7 +135,7 @@
        confirmButtonClass: 'command-message-box-confirm',
        cancelButtonClass: 'command-message-box-cancel',
      }).then(() => {
                logOutFun()
        logOutFun()
      })
    },
    jumpMH () {
@@ -249,14 +249,22 @@
            background: url('@/assets/images/active-menu-item.png') no-repeat center / 136px 34px;
            span {
              background: linear-gradient(180deg,
                  #fff 22.11%,
                  #ffffff 86.69%);
              background-size: contain;
              background-clip: text;
              -webkit-text-fill-color: transparent;
              /* 降级方案:所有浏览器先显示白色文字 */
              color: #ffffff;
              /* 发光效果 */
              text-shadow: 0px 0px 13px 0px #5da6ef73;
            }
            /* 支持渐变文字的浏览器再启用 */
            @supports (-webkit-background-clip: text) {
              span {
                background: linear-gradient(180deg, #fff 22.11%, #ffffff 86.69%);
                background-size: contain;
                background-clip: text;
                -webkit-text-fill-color: transparent;
              }
            }
          }
        }
      }
applications/mobile-web-view/README.md
@@ -29,3 +29,11 @@
## 目录约定
- 组件/页面使用 PascalCase 命名。
## 打包部署流程
打包,根目录下的 package.json , build:test:mobile-web-view
连接shuixiong服务器  目录 /software/service/drone/web/mobile-web-view
applications/mobile-web-view/src/appComponents/workMap/index.vue
@@ -5,12 +5,25 @@
                    <van-image width="20" height="20" :src="locationIcon" />
                    <div class="label">定位</div>
                </div>
                <div class="nav-btn" @click="openNavigation" v-show="navigationTarget">
                    <van-image width="20" height="20" :src="mapnavIcon" />
                    <div class="label">导航</div>
                </div>
                <van-action-sheet
                    v-model:show="navigationSheetShow"
                    :actions="navigationActions"
                    cancel-text="取消"
                    @select="handleNavigationSelect"
                />
    </div>
</template>
<script setup>
import { searchGeocoder } from '@/utils/util'
import { showToast } from 'vant'
import mapnavIcon from '@/appDataSource/appwork/mapnav.svg'
import incidentPoint from '@/appDataSource/appwork/positioning1.svg'
import userLocationIcon from '@/appDataSource/leafletMapIcon/user-location.svg'
@@ -51,6 +64,21 @@
    })
const currentAddress = ref('')
const AMAP_KEY = '5b8ba312d053e4bdb44911733ece7d63'
const navigationSheetShow = ref(false)
const navigationActions = [{ name: '高德地图' }, { name: '百度地图' }, { name: '腾讯地图' }]
const navigationTarget = computed(() => {
    if (!workNavigationShow || !mapCurrentDetail?.longitude || !mapCurrentDetail?.latitude) return null
    const lat = parseFloat(mapCurrentDetail.latitude)
    const lng = parseFloat(mapCurrentDetail.longitude)
    if (Number.isNaN(lat) || Number.isNaN(lng)) return null
    return {
        lat,
        lng,
        name: mapCurrentDetail.eventLocation || mapCurrentDetail.address || '工单位置',
    }
})
const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1])
const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3])
@@ -175,6 +203,42 @@
    }
}
function openNavigation() {
    if (!navigationTarget.value) {
        showToast('暂无可导航的位置')
        return
    }
    navigationSheetShow.value = true
}
function handleNavigationSelect(action) {
    navigationSheetShow.value = false
    const url = getNavigationUrl(action.name, navigationTarget.value)
    if (url) {
        window.location.href = url
    }
}
function getNavigationUrl(type, target) {
    const [gcjLng, gcjLat] = wgs84ToGcj02(target.lng, target.lat)
    const name = encodeURIComponent(target.name)
    if (type === '高德地图') {
        return `https://uri.amap.com/navigation?to=${gcjLng},${gcjLat},${name}&mode=car&policy=1&coordinate=gaode&callnative=1`
    }
    if (type === '百度地图') {
        return `https://api.map.baidu.com/direction?destination=latlng:${gcjLat},${gcjLng}|name:${name}&mode=driving&coord_type=gcj02&output=html&src=ja-web`
    }
    if (type === '腾讯地图') {
        return `https://apis.map.qq.com/uri/v1/routeplan?type=drive&tocoord=${gcjLat},${gcjLng}&to=${name}&referer=ja-web`
    }
    return ''
}
let lastLocationMarker = null
const getMapLocation = async () => {
    const customIcon = L.icon({
@@ -248,6 +312,11 @@
// 存储事件点标记实例,用于后续更新或移除
let incidentMarker = null
function formatCoordinate(value) {
    const numberValue = Number(value)
    return Number.isNaN(numberValue) ? value : numberValue.toFixed(6)
}
// 添加事件点标记
function addIncidentMarker(data) {
    // 检查地图和标记层是否已初始化
@@ -284,6 +353,19 @@
    // 存储关联数据
    incidentMarker.options.customData = data
    incidentMarker.bindTooltip(
        `<div class="coordinate-label">
            <div>经度:${formatCoordinate(lng)}</div>
            <div>纬度:${formatCoordinate(lat)}</div>
        </div>`,
        {
            permanent: true,
            direction: 'top',
            offset: L.point(0, -16),
            opacity: 1,
            className: 'incident-coordinate-tooltip',
        }
    )
    // 定位到该标记点
    mapSetView({
@@ -359,7 +441,8 @@
        width: 100%;
        height: 100%;
    }
    .location-btn{
    .location-btn,
    .nav-btn{
        display: flex;
        flex-direction: column;
        justify-content: center;
@@ -380,6 +463,9 @@
    .location-btn {
        bottom: 76px;
    }
    .nav-btn {
        bottom: 28px;
    }
}
// 位置标记闪烁效果
@@ -387,6 +473,29 @@
    animation: blink 1s ease-in-out;
}
:deep(.incident-coordinate-tooltip) {
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    &::before {
        display: none;
    }
}
:deep(.coordinate-label) {
    padding: 4px 7px;
    background: rgba(255, 255, 255, 0.95);
    border: 1px solid rgba(76, 133, 255, 0.35);
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    font-size: 11px;
    line-height: 16px;
    color: #222324;
    white-space: nowrap;
}
@keyframes blink {
    0% {
        transform: scale(1);
applications/mobile-web-view/src/appPages/work/workDetail/index.vue
@@ -2,12 +2,20 @@
    <div class="workDetailContainer">
        <div class="detailTop">
            <div class="image-container">
                <van-swipe :autoplay="3000" indicator-color="#4C85FF">
                <van-swipe-item v-for="(img, index) in getImageList" :key="index">
                    <van-image class="detailImage" :src="img" fit="cover" width="100%" height="235px"
                        @click="openPreview(index)" preview-visible="false" />
                </van-swipe-item>
            </van-swipe>
                <video
                    v-if="isVideoAttachment && mediaSrc"
                    ref="videoRef"
                    class="video-js vjs-default-skin vjs-big-play-centered detailVideo"
                    controls
                    preload="auto"
                    playsinline
                ></video>
                <van-swipe v-else :autoplay="3000" indicator-color="#4C85FF">
                    <van-swipe-item v-for="(img, index) in [mediaSrc]" :key="index">
                        <van-image class="detailImage" :src="img" fit="cover" width="100%" height="235px"
                            @click="openPreview(index)" preview-visible="false" />
                    </van-swipe-item>
                </van-swipe>
            </div>
        </div>
    <!-- 工单内容 -->
@@ -15,6 +23,10 @@
      <div class="workOrderContent">
        <div class="workOrderTitle">工单内容</div>
        <div class="workOrderContainer">
                    <div class="orderRow">
                        <div class="rowTitle">工单名称</div>
                        <div>{{ workDetailData.eventName }}</div>
                    </div>
          <div class="orderRow">
            <div class="rowTitle">工单编号</div>
            <div>{{ workDetailData.eventNum }}</div>
@@ -59,11 +71,18 @@
import { showToast, showNotify, showImagePreview } from 'vant'
import { getShowImg, getSmallImg } from '@/utils/util'
import { useRoute,useRouter } from 'vue-router'
import { getAiImg } from '@ztzf/utils'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
const keyword = ref('')
const route = useRoute()
const router = useRouter()
const workDetailData = ref({})
const videoRef = ref(null)
let player = null
const isVideoAttachment = computed(() => Number(workDetailData.value.attachmentType) === 3)
// 预览图片
const getImageList = computed(() => {
@@ -75,11 +94,10 @@
    }
    return imageArr
})
const openPreview = index => {
    const detail = workDetailData.value
    const showUrl = detail.eventImageUrl
const openPreview = () => {
    if (isVideoAttachment.value || !mediaSrc.value) return
    showImagePreview({
        images: [showUrl],
        images: [mediaSrc.value],
        startPosition: 0,
    })
}
@@ -88,21 +106,94 @@
router.push({
        path: '/webViewWrapper/mapWork',
        query: {
            currentItem: JSON.stringify(item)
            currentItem: JSON.stringify(item)
        }
    })
}
function replaceWithProxy(url) {
    if (!url) return ''
    return url.replace(
        /^https?:\/\/[^/]+/,
        'https://wrj.shuixiongit.com/ja-proxy'
    )
}
function getVideoType(url) {
    const path = (url || '').split('?')[0].toLowerCase()
    if (path.endsWith('.m3u8')) return 'application/x-mpegURL'
    if (path.endsWith('.mp4')) return 'video/mp4'
    if (path.endsWith('.webm')) return 'video/webm'
    if (path.endsWith('.ogg') || path.endsWith('.ogv')) return 'video/ogg'
    if (path.endsWith('.mov')) return 'video/quicktime'
    return ''
}
function getVideoSource() {
    const type = getVideoType(mediaSrc.value)
    return {
        src: mediaSrc.value,
        ...(type ? { type } : {}),
    }
}
function destroyPlayer() {
    if (!player) return
    player.dispose()
    player = null
}
async function initPlayer() {
    if (!isVideoAttachment.value || !mediaSrc.value) return
    await nextTick()
    if (!videoRef.value) return
    if (player) {
        player.src(getVideoSource())
        player.load()
        return
    }
    player = videojs(videoRef.value, {
        controls: true,
        preload: 'auto',
        autoplay: false,
        fluid: false,
        sources: [getVideoSource()],
    })
    player.on('error', () => {
        console.error('视频播放失败:', player?.error(), mediaSrc.value)
    })
}
const mediaSrc = ref('')
watch([isVideoAttachment, mediaSrc], () => {
    if (isVideoAttachment.value) {
        initPlayer()
    } else {
        destroyPlayer()
    }
})
onMounted(async () => {
    keyword.value = JSON.parse(route.query.workDetailData)
    try {
        const res = await getSharingDetailsApi({ cacheKey: keyword.value })
        workDetailData.value = res.data.data
        let {eventImageUrl,geojson} = workDetailData.value
        // http://220.177.172.27:8100 改为 https://wrj.shuixiongit.com/ja-proxy
        eventImageUrl = replaceWithProxy(eventImageUrl)
        if (eventImageUrl){
            mediaSrc.value = !isVideoAttachment.value && geojson ? await getAiImg(eventImageUrl,geojson) : eventImageUrl
        }
    } catch (error) {
        showToast('分享链接失效')
    }
})
onBeforeUnmount(destroyPlayer)
</script>
<style lang="scss" scoped>
.workDetailContainer {
@@ -110,12 +201,23 @@
        .image-container {
            position: relative;
            width: 100%;
            height: 235px;
            .detailImage {
                width: 100%;
                height: 100%;
                display: block;
                object-fit: cover;
            }
            .detailVideo {
                width: 100%;
                height: 235px;
            }
            :deep(.video-js .vjs-tech) {
                width: 100%;
                height: 100%;
            }
        }
    }
applications/task-work-order/README.md
@@ -25,3 +25,8 @@
## 资源与约定
- 组件/页面使用 PascalCase 命名。
- 静态资源放在 `public/` 或 `src/assets/`。
## 打包部署
根目录package.json  ,build:prod:task-work-order
手动上传打包后文件
连接服务器  /data/wwwtask
applications/task-work-order/src/components/VideoPlayDialog.vue
@@ -1,56 +1,130 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import videojs from 'video.js'
const props = defineProps({
    width: {
        type: String,
        default: '800'
        default: '800',
    },
    modelValue: {
        type: Boolean,
        default: false
        default: false,
    },
    playUrl: {
        type: String,
        default: ''
        default: '',
    },
    title:{
    title: {
        type: String,
        default: '视频播放'
    }
        default: '视频播放',
    },
})
const emit = defineEmits(['update:modelValue'])
const handleClose = () => {
    emit('update:modelValue', false)
const videoRefs = ref(null)
let player = null
const dialogVisible = computed({
    get: () => props.modelValue,
    set: value => emit('update:modelValue', value),
})
function getVideoType(url) {
    const path = (url || '').split('?')[0].toLowerCase()
    if (path.endsWith('.m3u8')) return 'application/x-mpegURL'
    if (path.endsWith('.mp4')) return 'video/mp4'
    if (path.endsWith('.webm')) return 'video/webm'
    if (path.endsWith('.ogg') || path.endsWith('.ogv')) return 'video/ogg'
    if (path.endsWith('.mov')) return 'video/quicktime'
    return ''
}
function destroyPlayer() {
    if (!player) return
    player.dispose()
    player = null
}
function getSource() {
    const type = getVideoType(props.playUrl)
    return {
        src: props.playUrl,
        ...(type ? { type } : {}),
    }
}
async function initPlayer() {
    if (!props.modelValue || !props.playUrl) return
    await nextTick()
    if (!videoRefs.value) return
    if (player) {
        player.src(getSource())
        player.load()
        return
    }
    player = videojs(videoRefs.value, {
        controls: true,
        preload: 'auto',
        autoplay: false,
        fluid: false,
        width: '100%',
        height: '600px',
        sources: [getSource()],
    })
    player.on('error', () => {
        console.error('视频播放失败:', player?.error(), props.playUrl)
    })
}
watch(
    () => props.playUrl,
    () => {
        if (!props.modelValue || !props.playUrl) return
        initPlayer()
    }
)
onBeforeUnmount(destroyPlayer)
</script>
<template>
    <el-dialog
        class="gd-dialog"
        class="gd-dialog video-dialog"
        :title="props.title"
        append-to-body
        v-model="props.modelValue"
        v-model="dialogVisible"
        width="60%"
        destroy-on-close
        :close-on-click-modal="false"
        @close="handleClose"
        @opened="initPlayer"
        @closed="destroyPlayer"
    >
        <video ref="videoRefs" controls preload="auto"  :src="props.playUrl">
            <source :src="props.playUrl" type="video/mp4" />
        </video>
        <!--            <el-carousel trigger="click" :autoplay="false" @change="handleCarouselChange">-->
        <!--                <el-carousel-item v-for="(item, index) in videoList" :key="index">-->
        <!--                    <video ref="videoRefs" controls preload="auto"  :src="currentVideoUrl" @play="handleVideoPlay" @ended="handleVideoEnded(index)">-->
        <!--                        <source :src="item.smallUrl" type="video/mp4" />-->
        <!--                    </video>-->
        <!--                </el-carousel-item>-->
        <!--            </el-carousel>-->
        <video
            ref="videoRefs"
            id="videoPlayDialogPlayer"
            class="video-js vjs-default-skin vjs-big-play-centered"
            controls
            preload="auto"
            playsinline
        ></video>
    </el-dialog>
</template>
<style lang="scss">
.video-dialog {
    .video-js {
        height: 600px;
        width: 100%;
    }
}
</style>
<style scoped lang="scss">
video {
    height: 600px;
:deep(.video-js .vjs-tech) {
    height: 100%;
    width: 100%;
}
</style>
applications/task-work-order/src/views/orderView/deviceInvocation/deviceCallRecord/FormDiaLog.vue
@@ -4,11 +4,7 @@
        <!--        <div class="detail-row-view" v-if="dialogReadonly">-->
        <el-table :data="detailsList" class="gd-dialog-table">
            <el-table-column type="index" show-overflow-tooltip label="序号" />
            <el-table-column prop="deviceStatus" show-overflow-tooltip label="设备状态" >
                <template v-slot="{ row }">
                    {{ getStatusText(row.deviceStatus) }}
                </template>
            </el-table-column>
            <el-table-column prop="deviceStatus" show-overflow-tooltip label="设备状态" />
            <el-table-column prop="occurTime" show-overflow-tooltip label="发生时间" />
        </el-table>
        <div class="gd-pagination-parent">
applications/task-work-order/src/views/orderView/flyingHand/FlyingHandDialog.vue
@@ -30,7 +30,7 @@
      </el-col>
      <el-col :span="12">
        <div class="label">擅长任务类型</div>
        <div class="val">{{ getDictLabel(formData.skilledTaskType, dictObj.workOrderType) }}</div>
        <div class="val">{{ getTaskTypeLabel(formData.skilledTaskType, workOrderTypeXT) }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">项目经验</div>
@@ -159,20 +159,17 @@
                </el-col>
                <el-col :span="12">
                    <el-form-item label="擅长任务类型" prop="skilledTaskType">
                        <el-select
                            class="gd-select"
                            popper-class="gd-select-popper"
                        <el-cascader
                            class="gd-cascader"
                            popper-class="gd-cascader-popper"
                            v-model="formData.skilledTaskType"
                            :options="workOrderTypeXT"
                            :props="{ ...taskTypeCascaderProps, multiple: true }"
                            placeholder="请选择"
                            :disabled="dialogType === 'view'"
                        >
                            <el-option
                                v-for="item in dictObj.workOrderType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                            clearable
                            collapse-tags
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="24">
@@ -207,6 +204,10 @@
import { submitFlyingHand } from './flyingHandApi'
import { ElMessage } from 'element-plus'
import { getDictLabel } from '@ztzf/utils'
import {
    getTaskTypeLabel,
    taskTypeCascaderProps,
} from '../orderManage/taskTypeOptions'
import { fieldRules } from '@ztzf/utils'
@@ -227,6 +228,7 @@
})
const dictObj = inject('dictObj') // 擅长任务类型
const workOrderTypeXT = inject('workOrderTypeXT', ref([]))
const flyingHandList = inject('flyingHandList') // 飞手列表
const flyingHandEditList = inject('flyingHandEditList') // 飞手建档列表
@@ -262,6 +264,7 @@
    projectExperience: '',
    technicalStrength: '',
    skilledUavType: '',
    skilledTaskType: [],
    certification: [], // 证书
    regionCode: '', // 区域编码
    flyerAddress: '', // 飞手地址
@@ -318,6 +321,7 @@
        if (newData) {
            Object.assign(formData.value, newData)
            formData.value.flightHours = Number(newData.flightHours)
            formData.value.skilledTaskType = newData.skilledTaskType || []
        }
    },
    { deep: true, immediate: true }
@@ -353,7 +357,7 @@
        projectExperience: '',
        technicalStrength: '',
        skilledUavType: '',
        skilledTaskType: ''
        skilledTaskType: []
    })
}
applications/task-work-order/src/views/orderView/flyingHand/index.vue
@@ -52,7 +52,7 @@
            <div class="gd-table-toolbar">
                <el-button :icon="Plus" color="#4C34FF" type="primary" @click="addFlyingHand">飞手建档</el-button>
            </div>
            <div class="gd-table-content gd-table-content-bg">
                <el-table class="gd-table" :data="list">
                    <el-table-column type="index" width="64" label="序号" />
@@ -72,7 +72,7 @@
                    </el-table-column>
                    <el-table-column prop="skilledTaskType" show-overflow-tooltip label="擅长任务类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.skilledTaskType, dictObj.workOrderType) }}
                            {{ getTaskTypeLabel(row.skilledTaskType, workOrderTypeXT) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" class-name="operation-btns" width="180">
@@ -113,10 +113,14 @@
import { getDictionaryByCode } from '@/api/system/dictbiz'
import { getDictLabel } from '@ztzf/utils'
import { getFlyingHandPage, removeFlyingHand,getAllFlyerIds } from './flyingHandApi'
import { deviceFlyerApi,deviceFlyerDetailApi } from '@/api/zkxt'
import { deviceFlyerApi,deviceFlyerDetailApi, getDictListApi } from '@/api/zkxt'
import FlyingHandDialog from './FlyingHandDialog.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { prod } from 'mathjs'
import {
    getTaskTypeLabel,
    normalizeTaskTypeOptions,
} from '../orderManage/taskTypeOptions'
// 初始化查询参数
const initSearchParams = () => ({
@@ -140,8 +144,10 @@
    skilledUavType: [], // 擅长机型
    technicalStrength: [] // 技术特长
})
const workOrderTypeXT = ref([])
provide('dictObj', dictObj)
provide('workOrderTypeXT', workOrderTypeXT)
// 获取列表
async function getList() {
@@ -229,6 +235,9 @@
    getDictionaryByCode('workOrderType,skilledUavType,technicalStrength').then(res => {
        dictObj.value = res.data.data
    })
    getDictListApi('task_inspection_type').then(res => {
        workOrderTypeXT.value = normalizeTaskTypeOptions(res.data.data || [])
    })
}
// 获取飞手列表
applications/task-work-order/src/views/orderView/orderManage/clueEvents/DistributeDiaLog.vue
@@ -11,6 +11,11 @@
        <el-form class="gd-dialog-form" ref="formRef" :model="formData" :rules="rules" label-width="100px">
            <el-row>
                <el-col :span="24">
                    <el-form-item label="事件名称" prop="eventName">
                        <el-input class="gd-input" v-model="formData.eventName" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="24">
                    <el-form-item label="处置部门" prop="disposeDept">
                        <el-tree-select
                            class="gd-select"
@@ -58,6 +63,7 @@
// 初始化表单数据
const initForm = () => ({
    eventName: '', // 事件名称
    disposeDept: '', // 处置部门
    disposeUser: '', // 工单处置人
})
@@ -76,6 +82,7 @@
// 校验规则
const rules = {
    eventName: fieldRules(true, 50),
    disposeDept: fieldRules(true),
    disposeUser: fieldRules(true),
}
@@ -109,6 +116,7 @@
            areaCode: currentRow.value.areaCode || '',
            disposeDept: formData.value.disposeDept,
            disposeUser: formData.value.disposeUser,
            eventName: formData.value.eventName,
            latitude: currentRow.value.latitude || 0,
            longitude: currentRow.value.longitude || 0,
            resultId: currentRow.value.id,
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue
@@ -5,14 +5,18 @@
                <el-table-column label="线索缩略图" width="120">
                    <template v-slot="{ row }">
                        <el-image
                            v-if="row.aiImg || row.resultUrl"
                            :src="row.aiImg || row.resultUrl"
                            :preview-src-list="[row.aiImg || row.resultUrl]"
                            v-if="row.attachmentType === 1 || row.attachmentType === 2"
                            :src="row.attachmentType === 1 ? row.resultUrl : row.aiImg"
                            :preview-src-list="[row.attachmentType === 1 ? row.resultUrl : row.aiImg]"
                            fit="cover"
                            style="width: 80px; height: 60px"
                            style="width: 80px; height: 80px"
                            preview-teleported
                        />
                        <span v-else>-</span>
                        <div class="video-btn" v-if="row.attachmentType === 3" @click="videoClick(row)">
                            <el-icon :size="30" color="#fff">
                                <VideoPlay />
                            </el-icon>
                        </div>
                    </template>
                </el-table-column>
                <el-table-column prop="resultCode" show-overflow-tooltip label="线索编号" />
@@ -31,8 +35,12 @@
                <el-table-column prop="distributeUserName" show-overflow-tooltip label="分发人员" />
                <el-table-column label="操作" class-name="operation-btns" width="140">
                    <template v-slot="{ row }">
                        <el-link type="primary" @click="openDistributeDialog(row)" v-if="permission.clueEvents_distribute && (row.distributeStatus === 0 || row.distributeStatus === 2)">
                            {{ row.distributeStatus === 2 ? '再次分发':'转为事件并分发' }}
                        <el-link
                            type="primary"
                            @click="openDistributeDialog(row)"
                            v-if="permission.clueEvents_distribute && (row.distributeStatus === 0 || row.distributeStatus === 2)"
                        >
                            {{ row.distributeStatus === 2 ? '再次分发' : '转为事件并分发' }}
                        </el-link>
                        <el-button disabled type="text" v-else>转为事件并分发</el-button>
                    </template>
@@ -51,12 +59,22 @@
            @success="getList"
        />
    </el-dialog>
    <VideoPlayDialog
        ref="videoPlayDialogRef"
        v-if="VideoShow"
        v-model="VideoShow"
        :playUrl="currentVideo.resultUrl"
    />
</template>
<script setup>
import { ref } from 'vue'
import { gdTaskResultListApi } from './achievementApi'
import DistributeDiaLog from './DistributeDiaLog.vue'
import { getAiImg } from '@ztzf/utils'
import { VideoPlay } from '@element-plus/icons-vue'
import VideoPlayDialog from '@/components/VideoPlayDialog.vue'
const store = useStore()
const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
@@ -71,6 +89,8 @@
const currentRow = ref(null) // 当前行数据
const distributeDialogRef = ref(null)
const distributeDialogVisible = ref(false)
const VideoShow = ref(false)
const currentVideo = ref({})
// 分发状态选项
const distributeStatusOptions = [
@@ -99,91 +119,18 @@
    if (!currentRow.value?.id) return
    loading.value = true
    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 }
        }))
        const res = await gdTaskResultListApi({ patrolTaskId: currentRow.value.id})
        list.value = await Promise.all(
            (res?.data?.data ?? []).map(async item => {
                if (item.attachmentType !== 2) return item
                const aiImg = await getAiImg(item.resultUrl,item.geojson)
                return { ...item, 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
    })
}
// 打开分发弹框
function openDistributeDialog(row) {
@@ -196,6 +143,12 @@
    })
}
// 点击视频
function videoClick(row) {
    currentVideo.value = row
    VideoShow.value = true
}
// 打开弹框
async function open({ row } = {}) {
    currentRow.value = row
@@ -206,8 +159,47 @@
</script>
<style lang="scss" scoped>
    .disabled-text {
        color: #C0C4CC;
        cursor: not-allowed;
.disabled-text {
    color: #c0c4cc;
    cursor: not-allowed;
}
.video-btn {
    width: 80px;
    height: 80px;
    position: relative;
    overflow: hidden;
    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%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    &::before {
        content: '';
        position: absolute;
        inset: 0;
        background-image: linear-gradient(
            90deg,
            rgba(76, 52, 255, 0.05) 0,
            rgba(76, 52, 255, 0.05) 1px,
            transparent 1px,
            transparent 12px
        );
        opacity: 0.42;
    }
    .el-icon {
        position: relative;
        z-index: 1;
        width: 42px;
        height: 42px;
        border-radius: 50%;
        background: rgba(76, 52, 255, 0.72);
        box-shadow: 0 4px 12px rgba(76, 52, 255, 0.2);
    }
}
</style>
applications/task-work-order/src/views/orderView/orderManage/clueEvents/index.vue
@@ -18,7 +18,7 @@
                    popper-class="gd-select-popper"
                    v-model="searchParams.patrolTaskType"
                    :options="workOrderTypeXT"
                    :props="taskTypeFilterCascaderProps"
                    :props="taskTypeCascaderProps"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
@@ -118,7 +118,11 @@
import { getDeptTree } from '@/api/system/dept'
import ViewDiaLog from './ViewDiaLog.vue'
import { useStore } from 'vuex'
import { getTaskTypeLabel, normalizeTaskTypeOptions, taskTypeFilterCascaderProps } from '../taskTypeOptions'
import {
    getTaskTypeLabel,
    normalizeTaskTypeOptions,
    taskTypeCascaderProps,
} from '../taskTypeOptions'
// 初始化查询参数
const initSearchParams = () => ({
@@ -164,6 +168,7 @@
            startTime: range[0],
            endTime: range[1],
            taskStatus: 8,
            patrolTaskType: searchParams?.value?.patrolTaskType?.join?.(','),
        })
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/index.vue
@@ -13,21 +13,16 @@
            </el-form-item>
            <el-form-item label="文档类型" prop="resultType">
                <el-select
                    class="gd-select gray"
                    popper-class="gd-select-popper"
                <el-cascader
                    class="gd-cascader gray"
                    popper-class="gd-cascader-popper"
                    v-model="searchParams.resultType"
                    :options="workOrderTypeXT"
                    :props="taskTypeCascaderProps"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.workOrderType"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
                />
            </el-form-item>
            <el-form-item class="gd-search-actions">
@@ -56,7 +51,7 @@
                    <!-- <el-table-column prop="resultType" show-overflow-tooltip label="文档类型" /> -->
                    <el-table-column prop="resultType" show-overflow-tooltip label="文档类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.resultType, dictObj.workOrderType) }}
                            {{ getTaskTypeLabel(row.resultType, workOrderTypeXT) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" class-name="operation-btns">
@@ -94,20 +89,16 @@
                    <el-input v-model="editParams.nickName" class="gd-input" placeholder="请输入" />
                </el-form-item>
                <el-form-item label="文档类型" prop="resultType" v-if="titleTxt === '上传巡查报告'">
                    <el-select
                        class="gd-select"
                        popper-class="gd-select-popper"
                    <el-cascader
                        class="gd-cascader"
                        popper-class="gd-cascader-popper"
                        v-model="editParams.resultType"
                        :options="workOrderTypeXT"
                        :props="{ ...taskTypeCascaderProps, multiple: true }"
                        placeholder="请选择"
                        clearable
                    >
                        <el-option
                            v-for="item in dictObj.workOrderType"
                            :key="item.dictKey"
                            :label="item.dictValue"
                            :value="item.dictKey"
                        />
                    </el-select>
                        collapse-tags
                    />
                </el-form-item>
                <el-form-item label="上传文件" prop="link" v-if="titleTxt === '上传巡查报告'">
                    <el-upload class="avatar-uploader" action="" :show-file-list="false" :before-upload="onUploadFileBefore">
@@ -142,15 +133,19 @@
    </basic-container>
</template>
<script setup>
import { Search, RefreshRight, Download, Upload, Delete } from '@element-plus/icons-vue'
import { fjPageApi, fjSubmitApi, fjRemoveApi, fjDetailApi, fjUploadApi,fjDownloadByByteApi } from './inspectionRequestApi'
import { Search, RefreshRight, Download, Upload } from '@element-plus/icons-vue'
import { fjPageApi, fjSubmitApi, fjRemoveApi, fjUploadApi, fjDownloadByByteApi } from './inspectionRequestApi'
import { useStore } from 'vuex'
import { ref, computed, inject, onMounted } from 'vue'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { getDictLabel } from '@ztzf/utils'
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PreviewFiles from '@/components/PreviewFiles/PreviewFiles.vue'
import { getDictionaryByCode } from '@/api/system/dictbiz'
import { Base64 } from 'js-base64'
import { getDictListApi } from '@/api/zkxt'
import {
    getTaskTypeLabel,
    normalizeTaskTypeOptions,
    taskTypeCascaderProps,
} from '../taskTypeOptions'
const store = useStore()
const userInfo = computed(() => store.getters.userInfo)
@@ -173,9 +168,7 @@
// 上传名称
const uploadName = ref('')
const dictObj = ref({
    workOrderType: [], // 巡查任务类型
})
const workOrderTypeXT = ref([])
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
@@ -203,9 +196,13 @@
// 获取字典
function getDictList() {
    getDictionaryByCode('workOrderType').then(res => {
        dictObj.value = res.data.data
    getDictListApi('task_inspection_type').then(res => {
        workOrderTypeXT.value = normalizeTaskTypeOptions(res.data.data || [])
    })
}
function toTaskTypeQueryValue(value) {
    return Array.isArray(value) ? value.join(',') : value
}
// 将字节大小转换为可读格式的函数
@@ -221,7 +218,10 @@
async function getList() {
    loading.value = true
    try {
        const res = await fjPageApi({ ...searchParams.value })
        const res = await fjPageApi({
            ...searchParams.value,
            resultType: toTaskTypeQueryValue(searchParams.value.resultType),
        })
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
@@ -401,7 +401,7 @@
        if (valid) {
            submitting.value = true
            try {
                await fjSubmitApi({ ...editParams.value })
                await fjSubmitApi(editParams.value)
                ElMessage.success('保存成功')
                isShowEditView.value = false
                getList()
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/FormDiaLog.vue
@@ -103,7 +103,7 @@
                                    popper-class="gd-select-popper"
                                    v-model="row.patrolTaskType"
                                    :options="workOrderTypeXT"
                                    :props="taskTypeCascaderProps"
                                    :props="{...taskTypeCascaderProps,multiple:true}"
                                    placeholder="请选择"
                                    collapse-tags
                                    @change="handlePatrolTaskTypeChange($index, $event)"
@@ -161,8 +161,7 @@
                                    v-model="row.deviceFlyerId"
                                    placeholder="请选择"
                                    filterable
                                    :disabled="dialogReadonly || !hasTaskTypeValue(taskList[$index].patrolTaskType)"
                                    :disabled="dialogReadonly || !taskList[$index].patrolTaskType"
                                >
                                    <el-option
                                        v-for="item in flyerListMap[$index] || []"
@@ -273,10 +272,7 @@
import { airlineListApi, algorithmGroupedApi } from '@/api/zkxt'
import {
    getTaskTypeLabel,
    getTaskTypeLeafValues,
    taskTypeCascaderProps,
    toTaskTypeCascaderValue,
    toTaskTypeRequestValue,
} from '../taskTypeOptions'
// 初始化任务行
@@ -342,14 +338,13 @@
        // 将工单类型赋值给所有子任务行
        const workOrderType = selectedWorkOrder.value?.workOrderType
        if (workOrderType) {
            const patrolTaskType = toTaskTypeCascaderValue(workOrderType)
            taskList.value.forEach((task, index) => {
                taskList.value[index] = {
                    ...initTaskRow(),
                    patrolTaskType,
                    patrolTaskType: workOrderType,
                }
                // 获取对应的飞手列表
                getFlyerList(index, patrolTaskType)
                getFlyerList(index, workOrderType)
            })
        }
    } else {
@@ -377,7 +372,7 @@
    const newTask = initTaskRow()
    // 如果已选择工单,自动填充工单类型
    if (selectedWorkOrder.value?.workOrderType) {
        newTask.patrolTaskType = toTaskTypeCascaderValue(selectedWorkOrder.value.workOrderType)
        newTask.patrolTaskType = selectedWorkOrder.value.workOrderType
    }
    const newIndex = taskList.value.length
    taskList.value.push(newTask)
@@ -441,8 +436,7 @@
// 获取飞手列表
async function getFlyerList(index, skilledTaskType) {
    const leafValues = getTaskTypeLeafValues(skilledTaskType)
    if (!leafValues.length) {
    if (!skilledTaskType.length) {
        flyerListMap.value[index] = []
        return
    }
@@ -450,7 +444,7 @@
    const res = await gdFlyerPageApi({
        size: 999,
        current: 1,
        skilledTaskType: leafValues.join(','),
        skilledTaskType: skilledTaskType.map(item => item[1])?.join?.(','),
        // longitude: obj.longitude || '',
        // latitude: obj.latitude || '',
    })
@@ -477,7 +471,6 @@
function isTaskRowFilled(task) {
    return !!(
        task.patrolTaskName ||
        hasTaskTypeValue(task.patrolTaskType) ||
        task.executeTime ||
        task.patrolRouteUrl ||
        task.deviceFlyerId ||
@@ -485,9 +478,6 @@
    )
}
function hasTaskTypeValue(value) {
    return !!toTaskTypeRequestValue(value).length
}
// 提交新增
async function handleSubmit() {
@@ -509,10 +499,6 @@
        // 已填写的行需要校验完整性
        if (!task.patrolTaskName) {
            ElMessage.warning(`第${i + 1}行: 请输入巡查任务名称`)
            return
        }
        if (!hasTaskTypeValue(task.patrolTaskType)) {
            ElMessage.warning(`第${i + 1}行: 请选择巡查任务类型`)
            return
        }
        if (!task.executeTime) {
@@ -538,7 +524,6 @@
        // 只提交已填写的任务行
        const dataList = filledTasks.map(item => ({
            ...item,
            patrolTaskType: toTaskTypeRequestValue(item.patrolTaskType),
            workOrderId: selectedWorkOrderId.value,
            id: undefined,
        }))
@@ -567,7 +552,7 @@
        // 编辑/查看模式 - 加载已有数据
        selectedWorkOrderId.value = row?.workOrderId
        handleWorkOrderChange(row?.workOrderId)
        taskList.value = [{ ...row, patrolTaskType: toTaskTypeCascaderValue(row?.patrolTaskType) }]
        taskList.value = [{ ...row, patrolTaskType: row?.patrolTaskType || [] }]
        flyerListMap.value = {}
        // 如果有巡查类型,加载对应的飞手列表
        if (row?.patrolTaskType) {
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -82,15 +82,19 @@
                            任务成果({{ taskResultList.length || 0 }}条)
                        </div>
                        <div class="imgBox">
                            <div v-for="item in taskResultList">
                            <div
                                v-for="item in taskResultList.filter(
                                    item1 => item1.resultUrl && [1, 2, 3].includes(item1.attachmentType)
                                )"
                            >
                                <el-image
                                    v-if="item.attachmentType === 0 && item.resultUrl"
                                    :src="item.resultUrl"
                                    :preview-src-list="[item.resultUrl]"
                                    v-if="item.attachmentType === 1 || item.attachmentType === 2"
                                    :src="item.attachmentType === 1 ? item.resultUrl : item.aiImg"
                                    :preview-src-list="[item.attachmentType === 1 ? item.resultUrl : item.aiImg]"
                                    fit="cover"
                                    preview-teleported
                                />
                                <div class="video-btn" v-if="item.attachmentType === 1 && item.resultUrl" @click="videoClick(item)">
                                <div class="video-btn" v-if="item.attachmentType === 3" @click="videoClick(item)">
                                    <el-icon :size="30" color="#fff">
                                        <VideoPlay />
                                    </el-icon>
@@ -116,8 +120,9 @@
                                    popper-class="gd-select-popper"
                                    v-model="formData.patrolTaskType"
                                    :options="workOrderTypeXT"
                                    :props="taskTypeCascaderProps"
                                    :props="{ ...taskTypeCascaderProps, multiple: true }"
                                    placeholder="请选择"
                                    collapse-tags
                                    clearable
                                />
                            </el-form-item>
@@ -274,31 +279,34 @@
        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'
import { gdTaskResultListApi } from '@/views/orderView/orderManage/clueEvents/achievementApi'
import RefuseOrderDialog1 from '@/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue'
import { Check } from '@element-plus/icons-vue'
import { Check, VideoPlay } from '@element-plus/icons-vue'
import { queryAirById, airlineListApi, algorithmGroupedApi } from '@/api/zkxt'
import * as Cesium from 'cesium'
import { useStore } from 'vuex'
import { getTaskTypeLabel, taskTypeCascaderProps, toTaskTypeCascaderValue, toTaskTypeRequestValue } from '../taskTypeOptions'
import { getTaskTypeLabel, taskTypeCascaderProps } from '../taskTypeOptions'
// 初始化表单数据
const initForm = () => ({
@@ -355,7 +363,7 @@
            children: (group.algorithms || []).map(alg => ({
                id: alg.id,
                name: alg.name,
            }))
            })),
        }))
    } catch (e) {
        console.error('获取算法列表失败', e)
@@ -366,10 +374,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 = {
@@ -489,11 +499,7 @@
    if (!isValid) return
    submitting.value = true
    try {
        const payload = {
            ...formData.value,
            patrolTaskType: toTaskTypeRequestValue(formData.value.patrolTaskType),
        }
        await gdPatrolTaskRepublish(payload)
        await gdPatrolTaskRepublish(formData.value)
        ElMessage.success('更新成功')
        visible.value = false
        emit('success')
@@ -508,8 +514,13 @@
    if (!formData.value?.id) return
    try {
        const res = await gdTaskResultListApi({ patrolTaskId: formData.value.id })
        taskResultList.value = res?.data?.data ?? []
        console.log(taskResultList.value, '8989')
        taskResultList.value = await Promise.all(
            (res?.data?.data ?? []).map(async item => {
                if (item.attachmentType !== 2) return item
                const aiImg = await getAiImg(item.resultUrl, item.geojson)
                return { ...item, aiImg }
            })
        )
    } finally {
    }
}
@@ -527,18 +538,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: {
@@ -584,7 +595,6 @@
    const res = await gdPatrolTaskDetailApi({ id: row.id })
    formData.value = { ...initForm(), ...res.data.data }
    formData.value.patrolTaskType = toTaskTypeCascaderValue(formData.value.patrolTaskType)
    // 确保 algorithmIds 是数组
    if (formData.value.algorithmIds && typeof formData.value.algorithmIds === 'string') {
        formData.value.algorithmIds = formData.value.algorithmIds.split(',').filter(Boolean)
@@ -596,7 +606,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()
@@ -624,8 +634,8 @@
let VideoShow = ref(false)
let currentVideo = ref({})
function videoClick(row) {
    VideoShow.value = true
    currentVideo.value = row
    VideoShow.value = true
}
// 初始化地图实例
@@ -669,6 +679,16 @@
        width: calc((100% - 40px) / 5); /* 5列布局,减去4个10px间隙 */
        height: 110px;
        flex-shrink: 0;
        position: relative;
        overflow: hidden;
        border-radius: 4px;
        cursor: pointer;
        box-shadow: inset 0 0 0 1px rgba(17, 31, 68, 0.08);
        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);
        }
    }
    .el-image {
        width: 100%;
@@ -677,11 +697,41 @@
    .video-btn {
        width: 100%;
        height: 100%;
        background-color: #9E9E9E;
        position: relative;
        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%);
        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        &::before {
            content: '';
            position: absolute;
            inset: 0;
            background-image: linear-gradient(
                90deg,
                rgba(76, 52, 255, 0.05) 0,
                rgba(76, 52, 255, 0.05) 1px,
                transparent 1px,
                transparent 12px
            );
            opacity: 0.42;
        }
        .el-icon {
            position: relative;
            z-index: 1;
            width: 42px;
            height: 42px;
            border-radius: 50%;
            background: rgba(76, 52, 255, 0.72);
            box-shadow: 0 4px 12px rgba(76, 52, 255, 0.2);
        }
    }
}
:deep(.el-timeline-item__timestamp) {
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue
@@ -6,28 +6,58 @@
        </el-tabs>
        <el-form ref="queryParamsRef" :model="searchParams" class="gd-search-form">
            <el-form-item label="任务名称" prop="patrolTaskName">
                <el-input class="gd-input gray" v-model="searchParams.patrolTaskName" placeholder="请输入" clearable
                    @clear="handleSearch" />
                <el-input
                    class="gd-input gray"
                    v-model="searchParams.patrolTaskName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item label="任务类型" prop="patrolTaskType">
                <el-cascader class="gd-cascader gray" popper-class="gd-cascader-popper" v-model="searchParams.patrolTaskType"
                    :options="workOrderTypeXT" :props="taskTypeFilterCascaderProps" placeholder="请选择" clearable
                    @change="handleSearch" />
                <el-cascader
                    class="gd-cascader gray"
                    popper-class="gd-cascader-popper"
                    v-model="searchParams.patrolTaskType"
                    :options="workOrderTypeXT"
                    :props="taskTypeCascaderProps"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                />
            </el-form-item>
            <el-form-item label="任务状态" prop="taskStatus">
                <el-select class="gd-select gray" popper-class="gd-select-popper" v-model="searchParams.taskStatus"
                    placeholder="请选择" clearable @change="handleSearch">
                    <el-option v-for="item in dictObj.taskStatus" :key="item.dictKey" :label="item.dictValue"
                        :value="item.dictKey" />
                <el-select
                    class="gd-select gray"
                    popper-class="gd-select-popper"
                    v-model="searchParams.taskStatus"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.taskStatus"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item label="执行时间" prop="executeTime">
                <el-date-picker class="gd-date-picker gray" popper-class="gd-date-picker-popper" v-model="dateRange"
                    type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
                    value-format="YYYY-MM-DD HH:mm:ss" @change="handleSearch" />
                <el-date-picker
                    class="gd-date-picker gray"
                    popper-class="gd-date-picker-popper"
                    v-model="dateRange"
                    type="daterange"
                    range-separator="-"
                    start-placeholder="开始日期"
                    end-placeholder="结束日期"
                    value-format="YYYY-MM-DD HH:mm:ss"
                    @change="handleSearch"
                />
            </el-form-item>
            <el-form-item class="gd-search-actions">
@@ -38,8 +68,7 @@
        <div class="gd-table-container" v-loading="loading">
            <div class="gd-table-toolbar">
                <el-button v-if="permission.flyOrder_add" :icon="Plus" color="#4C34FF" type="primary"
                    @click="openForm('add')">
                <el-button v-if="permission.flyOrder_add" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">
                    拆分工单
                </el-button>
            </div>
@@ -75,15 +104,25 @@
            </div>
            <div class="gd-pagination-parent">
                <el-pagination popper-class="gd-select-popper" v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size" layout="total, prev, pager, next, sizes" :total="total"
                    @change="getList" />
                <el-pagination
                    popper-class="gd-select-popper"
                    v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size"
                    layout="total, prev, pager, next, sizes"
                    :total="total"
                    @change="getList"
                />
            </div>
        </div>
        <FormDiaLog ref="dialogRef" @success="getList" v-if="dialogVisible" v-model="dialogVisible" />
        <ViewDiaLog ref="viewDiaLogRef" @success="getList" @refusalAccept="refusalAccept" v-if="viewDiaLogVisible"
            v-model="viewDiaLogVisible" />
        <ViewDiaLog
            ref="viewDiaLogRef"
            @success="getList"
            @refusalAccept="refusalAccept"
            v-if="viewDiaLogVisible"
            v-model="viewDiaLogVisible"
        />
    </basic-container>
</template>
<script setup>
@@ -97,7 +136,11 @@
import { gdPatrolTaskPageApi, gdPatrolTaskRemoveApi } from './inspectionRequestApi'
import ViewDiaLog from '@/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue'
import { useStore } from 'vuex'
import { getTaskTypeLabel, normalizeTaskTypeOptions, taskTypeFilterCascaderProps } from '../taskTypeOptions'
import {
    getTaskTypeLabel,
    normalizeTaskTypeOptions,
    taskTypeCascaderProps,
} from '../taskTypeOptions'
const store = useStore()
const permission = computed(() => store.state.user.permission)
@@ -136,28 +179,27 @@
provide('workOrderTypeXT', workOrderTypeXT)
const colors = {
    0: '#F6A000',//0待签收
    1: '#FF0202',//1拒绝签收
    2: '#FF0202',//2已撤回
    3: '#212BF4',//3待审核
    4: '#FF0000',//4审核驳回
    5: '#0068F0',//5审核通过
    6: '#FD6716',//6待验收
    7: '#FF0000',//7拒绝验收
    8: '#019612',//8验收通过
    0: '#F6A000', //0待签收
    1: '#FF0202', //1拒绝签收
    2: '#FF0202', //2已撤回
    3: '#212BF4', //3待审核
    4: '#FF0000', //4审核驳回
    5: '#0068F0', //5审核通过
    6: '#FD6716', //6待验收
    7: '#FF0000', //7拒绝验收
    8: '#019612', //8验收通过
}
// 获取列表
async function getList () {
async function getList() {
    const range = dateRangeFormat(dateRange.value)
    loading.value = true
    try {
        const res = await gdPatrolTaskPageApi({
            ...searchParams.value,
            startTime: range[0],
            endTime: range[1],
            createUser: activeName.value === 'my' ? store.state.user.userInfo.user_id : '',
            patrolTaskType: searchParams?.value?.patrolTaskType?.join?.(','),
        })
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
@@ -166,20 +208,20 @@
    }
}
function tabsClick (tab, event) {
function tabsClick(tab, event) {
    activeName.value = tab.props.name
    resetForm()
    handleSearch()
}
// 查询
function handleSearch () {
function handleSearch() {
    searchParams.value.current = 1
    getList()
}
// 重置查询
function resetForm () {
function resetForm() {
    queryParamsRef.value?.resetFields()
    dateRange.value = []
    searchParams.value.current = 1
@@ -187,7 +229,7 @@
}
// 新增/编辑/查看 弹框
function openForm (mode, row) {
function openForm(mode, row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode, row })
@@ -196,7 +238,7 @@
// 状态:0待签收、1拒绝签收、2已撤回、3待审核、4审核驳回
// 5审核通过、6待验收、7拒绝验收、8验收通过
function viewDiaLogView (row) {
function viewDiaLogView(row) {
    viewDiaLogVisible.value = true
    nextTick(() => {
        let mode = ['1', '4'].includes(row.taskStatus) && permission.value.flyOrder_add ? 'edit' : 'view'
@@ -205,13 +247,12 @@
}
// 勾选值设置
function handleSelectionChange (rows) {
function handleSelectionChange(rows) {
    selectedIds.value = rows.map(item => item.id)
}
// 获取字典
function getDictList () {
function getDictList() {
    getDictionaryByCode('workOrderType,deviceLoadDemand,taskStatus').then(res => {
        dictObj.value = res.data.data
    })
@@ -220,7 +261,7 @@
    })
}
function refusalAccept (row) {
function refusalAccept(row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode: 'edit', row })
@@ -228,7 +269,7 @@
}
// 获取两个tab的总条数
async function getTabTotals () {
async function getTabTotals() {
    const allRes = await gdPatrolTaskPageApi({
        current: 1,
        size: 1,
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
@@ -8,12 +8,17 @@
        destroy-on-close
        :close-on-click-modal="false"
    >
    <el-tabs class="gd-tabs"  v-model="activeName" @tab-click="tabsClick" v-if="dialogReadonly && formData.workOrderStatus >= '30'">
        <el-tabs
            class="gd-tabs"
            v-model="activeName"
            @tab-click="tabsClick"
            v-if="dialogReadonly && formData.workOrderStatus >= '30'"
        >
            <el-tab-pane label="基础信息" name="basic"></el-tab-pane>
            <el-tab-pane label="成果数据" name="result">
                <OutcomeData :workOrderId="formData.id" />
            </el-tab-pane>
            </el-tabs>
        </el-tabs>
        <div class="content" style="display: flex" v-if="activeName === 'basic'">
            <div class="processBox" v-if="dialogMode !== 'add'">
                <div class="detail-title">工单记录</div>
@@ -94,17 +99,16 @@
                        </el-col>
                        <el-col :span="12">
                            <el-form-item label="工单类型" prop="workOrderType">
                                <!-- <el-cascader
                                <el-cascader
                                    class="gd-cascader"
                                    popper-class="gd-cascader-popper"
                                    v-model="formData.workOrderType"
                                    :options="workOrderTypeXT"
                                    :props="taskTypeCascaderProps"
                                    collapse-tags
                                    :props="{...taskTypeCascaderProps,multiple:true }"
                                    placeholder="请选择"
                                    clearable
                                /> -->
                                <el-cascader class="gd-cascader" popper-class="gd-cascader-popper" v-model="formData.workOrderType"
                    :options="workOrderTypeXT" :props="taskTypeCascaderProps" placeholder="请选择" clearable collapse-tags />
                                    collapse-tags
                                />
                            </el-form-item>
                        </el-col>
                        <el-col :span="12">
@@ -130,12 +134,7 @@
                                    clearable
                                    @change="loadDemandChange"
                                >
                                    <el-option
                                        v-for="item in payloadList"
                                        :key="item.name"
                                        :label="item.name"
                                        :value="item.name"
                                    />
                                    <el-option v-for="item in payloadList" :key="item.name" :label="item.name" :value="item.name" />
                                </el-select>
                            </el-form-item>
                        </el-col>
@@ -176,9 +175,7 @@
                </div>
                <el-table
                    class="separateTable"
                    :empty-text="
                        pointList.length ? '暂无数据' : '请先在地图上绘制工单范围'
                    "
                    :empty-text="pointList.length ? '暂无数据' : '请先在地图上绘制工单范围'"
                    ref="deviceTableRef"
                    :data="pointList.length ? deviceList : []"
                    row-key="id"
@@ -284,7 +281,10 @@
import OrderStepBar from '@/views/orderView/orderManage/orderManage/OrderStepBar.vue'
import OutcomeData from './outcomeData.vue'
import { getDictListApi } from '@/api/zkxt'
import { getTaskTypeLabel, taskTypeCascaderProps, toTaskTypeCascaderValue, toTaskTypeRequestValue } from '../taskTypeOptions'
import {
    getTaskTypeLabel,
    taskTypeCascaderProps,
} from '../taskTypeOptions'
const store = useStore()
const permission = computed(() => store.state.user.permission)
const activeName = ref('basic')
@@ -412,7 +412,7 @@
async function getDeviceList() {
    try {
        const str = [...pointList.value, pointList.value[0]].map(item => `${item.longitude} ${item.latitude}`).join(',')
            let geom = `POLYGON((${str}))`
        let geom = `POLYGON((${str}))`
        const res = await gdManageDeviceListApi({
            deviceIds: dialogMode.value === 'add' ? '' : formData.value.recommendDeviceIds,
            devicePayload: formData.value?.deviceLoadDemand,
@@ -455,11 +455,7 @@
        if (!formData.value.id) {
            formData.value.workOrderStatus = '10'
        }
        const payload = {
            ...formData.value,
            workOrderType: toTaskTypeRequestValue(formData.value.workOrderType),
        }
        await gdWorkOrderSaveApi(payload)
        await gdWorkOrderSaveApi(formData.value)
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
        visible.value = false
        emit('success')
@@ -481,7 +477,6 @@
    loadList()
    const res = await gdWorkOrderDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? {}
    formData.value.workOrderType = toTaskTypeCascaderValue(formData.value.workOrderType)
    dateRange.value = [formData.value.executeStartTime, formData.value.executeEndTime]
}
@@ -700,15 +695,15 @@
</script>
<style lang="scss">
.orderManageFormDlg{
        .el-dialog__body{
            display: flex;
            flex-direction: column;
            .content{
                height: 0;
                flex: 1;
            }
.orderManageFormDlg {
    .el-dialog__body {
        display: flex;
        flex-direction: column;
        .content {
            height: 0;
            flex: 1;
        }
    }
}
</style>
@@ -725,7 +720,6 @@
        }
    }
}
.content {
    display: flex;
applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
@@ -6,28 +6,58 @@
        </el-tabs>
        <el-form ref="queryParamsRef" :model="searchParams" class="gd-search-form">
            <el-form-item label="工单名称" prop="workOrderName">
                <el-input class="gd-input gray" v-model="searchParams.workOrderName" placeholder="请输入" clearable
                    @clear="handleSearch" />
                <el-input
                    class="gd-input gray"
                    v-model="searchParams.workOrderName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item label="工单类型" prop="workOrderType">
                <el-cascader class="gd-cascader gray" popper-class="gd-cascader-popper" v-model="searchParams.workOrderType"
                    :options="workOrderTypeXT" :props="taskTypeFilterCascaderProps" placeholder="请选择" clearable
                    @change="handleSearch" />
                <el-cascader
                    class="gd-cascader gray"
                    popper-class="gd-cascader-popper"
                    v-model="searchParams.workOrderType"
                    :options="workOrderTypeXT"
                    :props="taskTypeCascaderProps"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                />
            </el-form-item>
            <el-form-item label="工单状态" prop="workOrderStatus">
                <el-select class="gd-select gray" popper-class="gd-select-popper" v-model="searchParams.workOrderStatus"
                    placeholder="请选择" clearable @change="handleSearch">
                    <el-option v-for="item in dictObj.workOrderStatus" :key="item.dictKey"
                        :label="item.dictValue.split('_')[1]" :value="item.dictKey" />
                <el-select
                    class="gd-select gray"
                    popper-class="gd-select-popper"
                    v-model="searchParams.workOrderStatus"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.workOrderStatus"
                        :key="item.dictKey"
                        :label="item.dictValue.split('_')[1]"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item label="执行时间" prop="executeTime">
                <el-date-picker class="gd-date-picker gray" popper-class="gd-date-picker-popper" v-model="dateRange"
                    type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
                    value-format="YYYY-MM-DD HH:mm:ss" @change="handleSearch" />
                <el-date-picker
                    class="gd-date-picker gray"
                    popper-class="gd-date-picker-popper"
                    v-model="dateRange"
                    type="daterange"
                    range-separator="-"
                    start-placeholder="开始日期"
                    end-placeholder="结束日期"
                    value-format="YYYY-MM-DD HH:mm:ss"
                    @change="handleSearch"
                />
            </el-form-item>
            <el-form-item class="gd-search-actions">
@@ -38,8 +68,7 @@
        <div class="gd-table-container" v-loading="loading">
            <div class="gd-table-toolbar">
                <el-button v-if="permission.order_release" :icon="Plus" color="#4C34FF" type="primary"
                    @click="openForm('add')">
                <el-button v-if="permission.order_release" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">
                    新增工单
                </el-button>
            </div>
@@ -85,9 +114,14 @@
            </div>
            <div class="gd-pagination-parent">
                <el-pagination popper-class="gd-select-popper" v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size" layout="total, prev, pager, next, sizes" :total="total"
                    @change="getList" />
                <el-pagination
                    popper-class="gd-select-popper"
                    v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size"
                    layout="total, prev, pager, next, sizes"
                    :total="total"
                    @change="getList"
                />
            </div>
        </div>
@@ -109,7 +143,11 @@
import testDock from '@/assets/docx/testDocx.docx'
import testPdf from '@/assets/pdf/testPdf.pdf'
import { useStore } from 'vuex'
import { getTaskTypeLabel, normalizeTaskTypeOptions, taskTypeFilterCascaderProps } from '../taskTypeOptions'
import {
    getTaskTypeLabel,
    normalizeTaskTypeOptions,
    taskTypeCascaderProps,
} from '../taskTypeOptions'
const store = useStore()
const activeName = ref('all')
@@ -148,7 +186,7 @@
provide('workOrderTypeXT', workOrderTypeXT)
// 获取列表
async function getList () {
async function getList() {
    const range = dateRangeFormat(dateRange.value)
    loading.value = true
    try {
@@ -157,6 +195,7 @@
            startTime: range[0],
            endTime: range[1],
            createUser: activeName.value === 'my' ? store.state.user.userInfo.user_id : '',
            workOrderType: searchParams?.value?.workOrderType?.join?.(','),
        })
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
@@ -166,27 +205,27 @@
}
// 查询
function handleSearch () {
function handleSearch() {
    searchParams.value.current = 1
    getList()
}
// 重置查询
function resetForm () {
function resetForm() {
    queryParamsRef.value?.resetFields()
    dateRange.value = []
    searchParams.value.current = 1
    getList()
}
function tabsClick (tab, event) {
function tabsClick(tab, event) {
    activeName.value = tab.props.name
    resetForm()
    handleSearch()
}
// 新增/编辑/查看 弹框
function openForm (mode, row) {
function openForm(mode, row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode, row })
@@ -196,7 +235,7 @@
// 21响应中_申请取消、22响应中_申请修改、23响应中_已取消、
// 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、
// 50验收通过_待结算、60结算完成_已结算
function openFormChange (row) {
function openFormChange(row) {
    dialogVisible.value = true
    nextTick(() => {
        const mode = ['11'].includes(row.workOrderStatus) && permission.value.order_release ? 'edit' : 'view'
@@ -223,7 +262,7 @@
}
// 删除
async function handleDelete (row) {
async function handleDelete(row) {
    const tips = row ? '该条' : '选中的项'
    await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', {
        type: 'warning',
@@ -239,12 +278,12 @@
}
// 勾选值设置
function handleSelectionChange (rows) {
function handleSelectionChange(rows) {
    selectedIds.value = rows.map(item => item.id)
}
// 获取字典
function getDictList () {
function getDictList() {
    getDictionaryByCode('deviceLoadDemand,workOrderType,workOrderStatus,taskStatus').then(res => {
        dictObj.value = res.data.data
    })
@@ -254,7 +293,7 @@
}
// 获取两个tab的总条数
async function getTabTotals () {
async function getTabTotals() {
    // 获取全部tab的总数
    const allRes = await gdWorkOrderPageApi({
        current: 1,
applications/task-work-order/src/views/orderView/orderManage/orderManage/outcomeData.vue
@@ -23,6 +23,24 @@
        </el-select>
      </el-form-item>
      <el-form-item label="成果类型" prop="attachmentType">
        <el-select
          class="gd-select"
              popper-class="gd-select-popper"
          v-model="searchParams.attachmentType"
          placeholder="请选择"
          clearable
          @change="handleSearch"
        >
          <el-option
            v-for="item in attachmentTypeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item >
        <el-button :icon="RefreshRight" @click="resetForm"></el-button>
        <el-button :icon="Search" color="#383874" @click="handleSearch"></el-button>
@@ -38,15 +56,15 @@
          <el-table-column type="index" label="序号" width="80" />
          <el-table-column label="图片/视频" >
            <template v-slot="{ row }">
              <el-image
                v-if="row.attachmentType ===0 && row.resultUrl"
                :src="row.resultUrl"
                :preview-src-list="[row.resultUrl]"
                fit="cover"
                style="width: 80px; height: 80px; border-radius: 4px;"
                                preview-teleported
              />
                            <div class="video-btn" v-if="row.attachmentType === 1 && row.resultUrl" @click="videoClick(row)">
                            <el-image
                                v-if="row.attachmentType === 1 || row.attachmentType === 2"
                                :src="row.attachmentType === 1 ? row.resultUrl : row.aiImg"
                                :preview-src-list="[row.attachmentType === 1 ? row.resultUrl : row.aiImg]"
                                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 />
                                </el-icon>
@@ -54,6 +72,12 @@
            </template>
          </el-table-column>
          <el-table-column prop="patrolTaskName" show-overflow-tooltip label="巡查任务名称" />
          <el-table-column label="成果类型">
            <template v-slot="{ row }">
              {{ getAttachmentTypeLabel(row.attachmentType) }}
            </template>
          </el-table-column>
          <el-table-column prop="shootTime" show-overflow-tooltip label="拍摄时间" />
          <el-table-column label="操作" class-name="operation-btns" width="120">
            <template v-slot="{ row }">
@@ -89,7 +113,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'
@@ -106,7 +130,8 @@
const initSearchParams = () => ({
  current: 1,
  size: 10,
  patrolTaskIds: []
  patrolTaskIds: [],
  attachmentType: ''
})
const searchParams = ref(initSearchParams())
@@ -117,6 +142,16 @@
const selectedRows = ref([])
const patrolTaskList = ref([])
const dictObj = inject('dictObj')
const attachmentTypeOptions = [
    { label: '媒体文件', value: 1 },
    { label: 'AI文件', value: 2 },
    { label: '主视频', value: 3 },
]
function getAttachmentTypeLabel(attachmentType) {
    return attachmentTypeOptions.find(item => item.value === Number(attachmentType))?.label || ''
}
// 导出加载状态
const exportLoading = ref(false)
// 获取巡查任务列表
@@ -140,7 +175,13 @@
        ? searchParams.value.patrolTaskIds.join(',')
        : ''
    })
    list.value = res?.data?.data?.records || []
        list.value = await Promise.all(
            (res?.data?.data?.records ?? []).map(async item => {
                if (item.attachmentType !== 2) return item
                const aiImg = await getAiImg(item.resultUrl,item.geojson)
                return { ...item, aiImg }
            })
        )
    total.value = res?.data?.data?.total || 0
  } finally {
    loading.value = false
@@ -246,8 +287,8 @@
let VideoShow = ref(false)
let currentVideo = ref({})
function videoClick(row) {
    currentVideo.value = row
    VideoShow.value = true
  currentVideo.value = row
}
// 监听 workOrderId 变化
applications/task-work-order/src/views/orderView/orderManage/taskTypeOptions.js
@@ -35,38 +35,9 @@
    )
}
function parseTaskTypeValue(value) {
    if (typeof value !== 'string') return value
    try {
        const result = JSON.parse(value)
        return Array.isArray(result) ? result : value
    } catch (error) {
        return value
    }
}
export function toTaskTypeCascaderValue(value) {
    value = parseTaskTypeValue(value)
    if (!value) return []
    if (typeof value === 'string') return []
    if (!Array.isArray(value)) return []
    if (isValidPath(value)) return [value]
    if (value.length === 1 && Array.isArray(value[0]) && !isValidPath(value[0])) {
        value = value[0]
    }
    return value.filter(isValidPath)
}
export function toTaskTypeRequestValue(value) {
    return toTaskTypeCascaderValue(value).map(item => [item[0], item[1]])
}
export function getTaskTypeLeafValues(value) {
    return toTaskTypeRequestValue(value).map(item => item[1])
}
export function getTaskTypeLabel(value, options = []) {
    const paths = toTaskTypeCascaderValue(value)
    if (!Array.isArray(value) || !value.length) return ''
    const paths = value.filter(isValidPath)
    if (paths.length) {
        return paths
            .map(path => {
@@ -77,29 +48,12 @@
            .join('、')
    }
    if (!value) return ''
    for (const item of options) {
        if (item.dictValue === value) return item.dictLabel
        const child = (item.children || []).find(option => option.dictValue === value)
        if (child) return child.dictLabel
    }
    return value
    return ''
}
export const taskTypeCascaderProps = {
    label: 'dictLabel',
    value: 'dictValue',
    children: 'children',
    multiple: true,
    emitPath: true,
    checkStrictly: false,
}
export const taskTypeFilterCascaderProps = {
    label: 'dictLabel',
    value: 'dictValue',
    children: 'children',
    emitPath: false,
    checkStrictly: false,
}
package.json
@@ -1,7 +1,7 @@
{
    "name": "my-monorepo",
    "private": true,
    "packageManager": "pnpm@9.0.0",
    "packageManager": "pnpm@9.15.9",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
packages/utils/common/index.js
@@ -73,3 +73,69 @@
            return { longitude, latitude }
        })
}
// 图片转带ai框的图片
export function getAiImg(url, aiFrameSource) {
    if (!url) return url
    if (!aiFrameSource) return url
    // let url = 'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fportrait%2F165a01e1cd859d59133b7755623a6c29.png&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1780851600&t=d5ce5f611eafadd932b84ef14275fbda'
    // const aiFrame1 = '[{"score":0.89990234375,"bbox":{"x_cen":195.5,"y_cen":326.5,"width":117.0,"height":265.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.89306640625,"bbox":{"x_cen":1194.5,"y_cen":559.5,"width":115.0,"height":261.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.88720703125,"bbox":{"x_cen":179.0,"y_cen":955.5,"width":124.0,"height":249.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.88330078125,"bbox":{"x_cen":1198.5,"y_cen":260.5,"width":115.0,"height":285.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.84716796875,"bbox":{"x_cen":204.5,"y_cen":71.5,"width":115.0,"height":143.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.83203125,"bbox":{"x_cen":186.0,"y_cen":657.5,"width":114.0,"height":269.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"},{"score":0.78662109375,"bbox":{"x_cen":1205.5,"y_cen":49.5,"width":117.0,"height":99.0},"class_name":"car","algorithmId":"e71116098eeb1d60cfebd04d30653b151"}]'
    const aiFrame = JSON.parse(aiFrameSource)
    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
                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
    })
}
pnpm-lock.yaml
@@ -395,7 +395,7 @@
        version: 3.5.27
      postcss-pxtorem:
        specifier: 'catalog:'
        version: 6.1.0(postcss@8.5.6)
        version: 6.1.0(postcss@5.2.18)
      sass:
        specifier: 'catalog:'
        version: 1.97.2
@@ -947,6 +947,9 @@
      '@dcloudio/uni-ui':
        specifier: ^1.5.11
        version: 1.5.11
      '@ztzf/utils':
        specifier: workspace:*
        version: link:../../packages/utils
      dayjs:
        specifier: ^1.11.18
        version: 1.11.19
@@ -2035,8 +2038,8 @@
  '@dcloudio/uni-app-vue@3.0.0-4070520250711001':
    resolution: {integrity: sha512-/ISF+1xrcvUtcote+LfLyk/pyizTjEY6/3HZgBsOfXchkZBz0fkTWgnj5RHYeqwD7aHLsOJX93eaazNMnzD9Zw==}
  '@dcloudio/uni-app-x@0.7.100':
    resolution: {integrity: sha512-aiRJ/A5Gak24aYKbPrEB6dJycqLpL8/24hqiUOezxFcEYVjNn18UBHSwGGuFUNRw3j3RhEaFzPL5n+reQTXRjg==}
  '@dcloudio/uni-app-x@0.7.124':
    resolution: {integrity: sha512-QqiIa0t7kg6xweh58QgiEx0zX6d0CBzkb8TJz3rx8N2cTH5k2w32iSyQaDzfjSmqQITZDbWNlBxlyWUY0i4pyA==}
  '@dcloudio/uni-app@3.0.0-4070520250711001':
    resolution: {integrity: sha512-rR1SGJknJyDy80vb47yDICXcZrV11KJ5yhNG8aC+kH2ZL338faBrdOQgqqN/XJegVxJdTfv1KqUXuaShMWTZGA==}
@@ -7843,6 +7846,7 @@
  prebuild-install@7.1.3:
    resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
    engines: {node: '>=10'}
    deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
    hasBin: true
  prelude-ls@1.2.1:
@@ -10582,7 +10586,7 @@
  '@dcloudio/uni-app-vue@3.0.0-4070520250711001': {}
  '@dcloudio/uni-app-x@0.7.100': {}
  '@dcloudio/uni-app-x@0.7.124': {}
  '@dcloudio/uni-app@3.0.0-4070520250711001(@dcloudio/types@3.4.29)(@nuxt/kit@3.20.2)(@vueuse/core@9.13.0(vue@3.4.21(typescript@5.9.3)))(postcss@8.5.6)(vue@3.4.21(typescript@5.9.3))':
    dependencies:
@@ -11089,7 +11093,7 @@
  '@dcloudio/uni-uts-v1@3.0.0-4070520250711001':
    dependencies:
      '@babel/code-frame': 7.28.6
      '@dcloudio/uni-app-x': 0.7.100
      '@dcloudio/uni-app-x': 0.7.124
      '@dcloudio/uts': 3.0.0-4070520250711001
      '@rollup/pluginutils': 5.3.0(rollup@4.55.3)
      '@vue/shared': 3.4.21
@@ -18273,6 +18277,10 @@
    dependencies:
      postcss: 5.2.18
  postcss-pxtorem@6.1.0(postcss@5.2.18):
    dependencies:
      postcss: 5.2.18
  postcss-pxtorem@6.1.0(postcss@8.5.6):
    dependencies:
      postcss: 8.5.6
uniapps/work-app/package.json
@@ -1,5 +1,5 @@
{
    "name": "work-app",
    "name": "@ztzf/work-app",
    "type": "module",
    "version": "1.6.0",
    "description": "吉安低空",
@@ -64,13 +64,14 @@
        "@dcloudio/uni-mp-xhs": "3.0.0-4070520250711001",
        "@dcloudio/uni-quickapp-webview": "3.0.0-4070520250711001",
        "@dcloudio/uni-ui": "^1.5.11",
        "@ztzf/utils": "workspace:*",
        "dayjs": "^1.11.18",
        "deep-pick-omit": "1.2.1",
        "destr": "2.0.5",
        "js-audio-recorder": "^1.0.7",
        "js-base64": "^3.7.4",
        "js-md5": "^0.7.3",
        "leaflet": "^1.9.4",
        "deep-pick-omit": "1.2.1",
        "destr": "2.0.5",
        "pinia": "2.2.4",
        "pinia-plugin-persistedstate": "4.1.3",
        "trtc-sdk-v5": "^5.14.1",
@@ -84,6 +85,7 @@
        "@dcloudio/uni-automator": "3.0.0-4070520250711001",
        "@dcloudio/uni-cli-shared": "3.0.0-4070520250711001",
        "@dcloudio/uni-stacktracey": "3.0.0-4070520250711001",
        "@dcloudio/uni-uts-v1": "3.0.0-4070520250711001",
        "@dcloudio/vite-plugin-uni": "3.0.0-4070520250711001",
        "@esbuild/darwin-arm64": "0.25.1",
        "@esbuild/darwin-x64": "0.25.1",
uniapps/work-app/src/config/env.js
@@ -12,8 +12,8 @@
const development = {
  VITE_APP_ENV:'development',
  // 开发环境这里改为自己的
  // VITE_APP_WEBVIEW_URL: 'https://wrj.shuixiongit.com/mobile-web-view/#/webViewWrapper',
  VITE_APP_WEBVIEW_URL: 'https://192.168.1.157:5179/mobile-web-view/#/webViewWrapper',
    // VITE_APP_WEBVIEW_URL: 'https://192.168.1.157:5179/mobile-web-view/#/webViewWrapper',
  VITE_APP_WEBVIEW_URL: 'https://wrj.shuixiongit.com/mobile-web-view/#/webViewWrapper',
  VITE_API_BASE_URL: 'http://220.177.172.27:8100/yisuanzhijian',
  VITE_API_BASE_URL_GD: 'http://220.177.172.27:8100/api/',
  // VITE_API_BASE_URL: 'https://aisky.org.cn/api',
uniapps/work-app/src/manifest.json
@@ -30,7 +30,10 @@
        },
        /* 模块配置 */
        "modules" : {
            "Share" : {}
            "Share" : {},
            "Geolocation" : {},
            "VideoPlayer" : {},
            "Camera" : {}
        },
        /* 应用发布信息 */
        "distribute" : {
@@ -73,6 +76,11 @@
                        "appid" : "gh_f2fbbd1d5d2a",
                        "UniversalLinks" : ""
                    }
                },
                "geolocation" : {
                    "system" : {
                        "__platform__" : [ "android" ]
                    }
                }
            },
            "icons" : {
@@ -113,6 +121,9 @@
        "router" : {
            "mode" : "hash",
            "base" : "/work-app/"
        },
        "sdkConfigs" : {
            "maps" : {}
        }
    },
    "locale" : "zh-Hans",
uniapps/work-app/src/pages/work/index.vue
@@ -12,10 +12,18 @@
      <scroll-view class="eventBox" scroll-y :lower-threshold="80" >
        <div class="eventList">
          <div class="eventItem"  v-for="(item,index) in dataList" :key="index">
            <img :src="item.eventImageUrl" alt="" @click="detailHandle(item)" />
            <image
                            v-if="[1, 2].includes(item.attachmentType)"
                            :src="item?.aiImg || item?.eventImageUrl"
                            mode="aspectFill"
                            @click="detailHandle(item)"
                        />
                        <div v-if="item.attachmentType === 3" class="videoBox" @click="detailHandle(item)">
                            <div class="playIcon"></div>
                        </div>
            <div class="informationDisplay">
              <!--              <div class="itemTitle">{{item.event_name}}</div>-->
              <div class="itemContent">{{formatDate(item.createTime) }}</div>
                            <div class="itemTitle">{{ item.eventName }}</div>
              <div class="itemContent">{{ formatDate(item.createTime) }}</div>
            </div>
          </div>
        </div>
@@ -32,6 +40,12 @@
        </div>
      </scroll-view>
    </div>
        <canvas
            canvas-id="ai-image-canvas"
            id="ai-image-canvas"
            class="ai-image-canvas"
            :style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
        ></canvas>
  </div>
@@ -39,10 +53,13 @@
<script setup>
import { getCurrentInstance, nextTick } from 'vue'
import { useUserStore } from '@/store/index.js'
import {getGdList,getstatusCount} from '/src/api/work/index.js'
import {getGdList,getstatusCount} from '@/api/work/index.js'
import dayjs from 'dayjs';
import { getStatusBarHeight } from '@/utils/common';
const canvasOwner = getCurrentInstance()?.proxy
const userStore = useUserStore()
const userInfo = userStore.userInfo
const dataList = ref([])
@@ -82,6 +99,106 @@
  keyword:''
})
const mockGeojson = "[{\"score\":0.89990234375,\"bbox\":{\"x_cen\":195.5,\"y_cen\":326.5,\"width\":117.0,\"height\":265.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.89306640625,\"bbox\":{\"x_cen\":1194.5,\"y_cen\":559.5,\"width\":115.0,\"height\":261.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.88720703125,\"bbox\":{\"x_cen\":179.0,\"y_cen\":955.5,\"width\":124.0,\"height\":249.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.88330078125,\"bbox\":{\"x_cen\":1198.5,\"y_cen\":260.5,\"width\":115.0,\"height\":285.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.84716796875,\"bbox\":{\"x_cen\":204.5,\"y_cen\":71.5,\"width\":115.0,\"height\":143.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.83203125,\"bbox\":{\"x_cen\":186.0,\"y_cen\":657.5,\"width\":114.0,\"height\":269.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"},{\"score\":0.78662109375,\"bbox\":{\"x_cen\":1205.5,\"y_cen\":49.5,\"width\":117.0,\"height\":99.0},\"class_name\":\"car\",\"algorithmId\":\"e71116098eeb1d60cfebd04d30653b151\"}]"
const canvasSize = ref({
    width: 1,
    height: 1
})
const parseAiFrame = aiFrameSource => {
    if (!aiFrameSource) return []
    if (Array.isArray(aiFrameSource)) return aiFrameSource
    try {
        return JSON.parse(aiFrameSource)
    } catch (error) {
        console.log(error)
        return []
    }
}
const getImageInfo = src =>
    new Promise(resolve => {
        uni.getImageInfo({
            src,
            success: resolve,
            fail: error => {
                console.log(error)
                resolve(null)
            }
        })
    })
const canvasToTempFilePath = (width, height) =>
    new Promise(resolve => {
        uni.canvasToTempFilePath({
            canvasId: 'ai-image-canvas',
            width,
            height,
            destWidth: width,
            destHeight: height,
            fileType: 'jpg',
            quality: 0.92,
            success: res => resolve(res.tempFilePath),
            fail: error => {
                console.log(error)
                resolve('')
            }
        }, canvasOwner)
    })
const drawAiImage = async (url, aiFrameSource) => {
    if (!url) return url
    const aiFrame = parseAiFrame(aiFrameSource)
    if (!aiFrame.length) return url
    const imageInfo = await getImageInfo(url)
    if (!imageInfo?.path || !imageInfo.width || !imageInfo.height) return url
    const width = Math.round(imageInfo.width)
    const height = Math.round(imageInfo.height)
    canvasSize.value = { width, height }
    await nextTick()
    const ctx = uni.createCanvasContext('ai-image-canvas', canvasOwner)
    ctx.drawImage(imageInfo.path, 0, 0, width, height)
    aiFrame.forEach(item => {
        const { x_cen, y_cen, width: boxWidth, height: boxHeight } = item.bbox || {}
        if ([x_cen, y_cen, boxWidth, boxHeight].some(value => typeof value !== 'number')) return
        const x = x_cen - boxWidth / 2
        const y = y_cen - boxHeight / 2
        const label = item.class_name || ''
        const fontSize = Math.max(18, Math.round(width / 80))
        const labelHeight = fontSize + 10
        const labelY = y - labelHeight >= 0 ? y - labelHeight : y
        const lineWidth = Math.max(3, Math.round(width / 640))
        ctx.setStrokeStyle('#FF3B30')
        ctx.setLineWidth(lineWidth)
        ctx.strokeRect(x, y, boxWidth, boxHeight)
        if (label) {
            ctx.setFontSize(fontSize)
            const labelWidth = label.length * fontSize + 16
            ctx.setFillStyle('#FF3B30')
            ctx.fillRect(x, labelY, labelWidth, labelHeight)
            ctx.setFillStyle('#FFFFFF')
            ctx.setTextBaseline('middle')
            ctx.fillText(label, x + 8, labelY + labelHeight / 2)
        }
    })
    return new Promise(resolve => {
        ctx.draw(false, async () => {
            const tempFilePath = await canvasToTempFilePath(width, height)
            resolve(tempFilePath || url)
        })
    })
}
const getDataList = () => {
  if (loading.value || !hasMore.value) return
@@ -92,9 +209,19 @@
    keyword:listParams.value.keyword,
    onlyMine:currentTab.value=== 'myTickets' ? 1 : 0
  }
  getGdList(params).then(res => {
  getGdList(params).then(async res => {
    const response = res.data.data
    dataList.value = response
        const list = []
        for (const item of response) {
            // const aiImg = await drawAiImage(item.eventImageUrl, mockGeojson)
            if (item.attachmentType !== 2) {
                list.push(item)
            }else{
                const aiImg = await drawAiImage(item.eventImageUrl, item.geojson)
                list.push({ ...item, aiImg })
            }
        }
        dataList.value = list
    // 根据当前页码决定是替换还是追加数据
    // if (listParams.value.current === 1) {
    //   dataList.value = response
@@ -150,7 +277,7 @@
const detailHandle = (val) => {
  uni.navigateTo({
    url: `/subPackages/workDetail/index?id=${val.id}`,
    url: `/subPackages/workDetail/index?id=${val.id}` + (val.aiImg ? `&aiImg=${val.aiImg}` : ''),
  })
}
@@ -261,12 +388,40 @@
      box-sizing: border-box;
      max-width: 100%;
      img {
      image,
            .videoBox {
        width: 100%;
        height: 208rpx;
        border-radius: 12rpx;
        overflow: hidden;
                display: block;
      }
            .videoBox {
                background: linear-gradient(135deg, #eef4ff 0%, #dfe8f8 100%);
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .playIcon {
                width: 72rpx;
                height: 72rpx;
                border-radius: 50%;
                background: rgba(29, 111, 233, 0.9);
                position: relative;
                box-shadow: 0 8rpx 18rpx rgba(29, 111, 233, 0.24);
                &::after {
                    content: '';
                    position: absolute;
                    left: 29rpx;
                    top: 20rpx;
                    width: 0;
                    height: 0;
                    border-top: 16rpx solid transparent;
                    border-bottom: 16rpx solid transparent;
                    border-left: 22rpx solid #fff;
                }
            }
      .informationDisplay{
        width: 100%;
        position: absolute;
@@ -275,14 +430,16 @@
        border-radius: 0rpx 0rpx 12rpx 12rpx;
        display: flex;
        align-items: center;
        justify-content: flex-end;
        padding: 10rpx 0rpx 10rpx 0rpx;
        justify-content: space-between;
        padding: 10rpx 14rpx;
                box-sizing: border-box;
        .itemTitle {
          width: 144rpx;
          flex: 1;
                    min-width: 0;
          font-family: Source Han Sans CN, Source Han Sans CN;
          font-weight: 500;
          font-size: 28rpx;
          color: #000000;
          font-size: 24rpx;
          color: #FFFFFF;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
@@ -293,7 +450,8 @@
          font-weight: 400;
          font-size: 24rpx;
          color: #FFFFFF;
          padding-right: 14rpx;
          margin-left: 12rpx;
                    white-space: nowrap;
        }
      }
@@ -319,5 +477,12 @@
    color: #999;
    width: 100%;
  }
    .ai-image-canvas {
        position: fixed;
        left: -9999px;
        top: -9999px;
        pointer-events: none;
    }
}
</style>
uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -30,6 +30,10 @@
        <div class="workOrderTitle">工单内容</div>
        <div class="workOrderContainer">
          <div class="orderRow">
            <div class="rowTitle">工单名称</div>
            <div>{{ workDetailData.eventName }}</div>
          </div>
          <div class="orderRow">
            <div class="rowTitle">工单编号</div>
            <div>{{ workDetailData.eventNum }}</div>
          </div>
@@ -116,17 +120,17 @@
const viewUrl = ref("");
onLoad(async options => {
  eventNum.value = options.id
  await getDataList(options.id)
  await getDataList(options.id,options.aiImg)
  checkEnvironment()
  viewUrl.value = getWebViewUrl("/workDetail", { workDetailData: JSON.stringify(workDetailData.value) });
})
const getDataList = async val => {
const getDataList = async (val,aiImg) => {
  const params = {
    id: val,
  }
  const res = await getGddetailedData(params)
  const response = res.data.data
  workDetailData.value = response
  workDetailData.value = {...response,aiImg}
}
// 检查运行环境
const checkEnvironment = () => {
@@ -154,10 +158,8 @@
  const imageArr = []
  const detail = workDetailData.value
  if (detail.eventImageUrl) {
    const smallUrl = detail.eventImageUrl
    imageArr.push(smallUrl)
    imageArr.push(detail.aiImg || detail.eventImageUrl)
  }
  return imageArr
})
// 跳转地图