Merge remote-tracking branch 'origin/master'
| | |
| | | |
| | | ## 环境要求 |
| | | - Node.js >= 20 |
| | | - pnpm >= 9 |
| | | - pnpm >= 9.15.9 |
| | | |
| | | ## 快速开始 |
| | | ```bash |
| | |
| | | - `applications/drone-app-web-view/README.md` |
| | | |
| | | ## 备注 |
| | | - 目前未配置测试框架,`pnpm test` 会直接退出。 |
| | | - 目前未配置测试框架,`pnpm test` 会直接退出。 |
| | |
| | | <!-- 高德地图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> |
| | |
| | | |
| | | <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> |
| | |
| | | confirmButtonClass: 'command-message-box-confirm', |
| | | cancelButtonClass: 'command-message-box-cancel', |
| | | }).then(() => { |
| | | logOutFun() |
| | | logOutFun() |
| | | }) |
| | | }, |
| | | jumpMH () { |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | ## 目录约定 |
| | | - 组件/页面使用 PascalCase 命名。 |
| | | |
| | | |
| | | |
| | | ## 打包部署流程 |
| | | |
| | | 打包,根目录下的 package.json , build:test:mobile-web-view |
| | | |
| | | 连接shuixiong服务器 目录 /software/service/drone/web/mobile-web-view |
| | |
| | | <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' |
| | |
| | | }) |
| | | 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]) |
| | |
| | | } |
| | | } |
| | | |
| | | 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({ |
| | |
| | | // 存储事件点标记实例,用于后续更新或移除 |
| | | let incidentMarker = null |
| | | |
| | | function formatCoordinate(value) { |
| | | const numberValue = Number(value) |
| | | return Number.isNaN(numberValue) ? value : numberValue.toFixed(6) |
| | | } |
| | | |
| | | // 添加事件点标记 |
| | | function addIncidentMarker(data) { |
| | | // 检查地图和标记层是否已初始化 |
| | |
| | | |
| | | // 存储关联数据 |
| | | 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({ |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .location-btn{ |
| | | .location-btn, |
| | | .nav-btn{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | |
| | | .location-btn { |
| | | bottom: 76px; |
| | | } |
| | | .nav-btn { |
| | | bottom: 28px; |
| | | } |
| | | } |
| | | |
| | | // 位置标记闪烁效果 |
| | |
| | | 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); |
| | |
| | | <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> |
| | | <!-- 工单内容 --> |
| | |
| | | <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> |
| | |
| | | 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(() => { |
| | |
| | | } |
| | | 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, |
| | | }) |
| | | } |
| | |
| | | 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 { |
| | |
| | | .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%; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | ## 资源与约定 |
| | | - 组件/页面使用 PascalCase 命名。 |
| | | - 静态资源放在 `public/` 或 `src/assets/`。 |
| | | |
| | | ## 打包部署 |
| | | 根目录package.json ,build:prod:task-work-order |
| | | 手动上传打包后文件 |
| | | 连接服务器 /data/wwwtask |
| | |
| | | <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> |
| | |
| | | <!-- <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"> |
| | |
| | | </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> |
| | |
| | | </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"> |
| | |
| | | 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' |
| | | |
| | |
| | | }) |
| | | |
| | | const dictObj = inject('dictObj') // 擅长任务类型 |
| | | const workOrderTypeXT = inject('workOrderTypeXT', ref([])) |
| | | |
| | | const flyingHandList = inject('flyingHandList') // 飞手列表 |
| | | const flyingHandEditList = inject('flyingHandEditList') // 飞手建档列表 |
| | |
| | | projectExperience: '', |
| | | technicalStrength: '', |
| | | skilledUavType: '', |
| | | skilledTaskType: [], |
| | | certification: [], // 证书 |
| | | regionCode: '', // 区域编码 |
| | | flyerAddress: '', // 飞手地址 |
| | |
| | | if (newData) { |
| | | Object.assign(formData.value, newData) |
| | | formData.value.flightHours = Number(newData.flightHours) |
| | | formData.value.skilledTaskType = newData.skilledTaskType || [] |
| | | } |
| | | }, |
| | | { deep: true, immediate: true } |
| | |
| | | projectExperience: '', |
| | | technicalStrength: '', |
| | | skilledUavType: '', |
| | | skilledTaskType: '' |
| | | skilledTaskType: [] |
| | | }) |
| | | } |
| | | |
| | |
| | | <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="序号" /> |
| | |
| | | </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"> |
| | |
| | | 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 = () => ({ |
| | |
| | | skilledUavType: [], // 擅长机型 |
| | | technicalStrength: [] // 技术特长 |
| | | }) |
| | | const workOrderTypeXT = ref([]) |
| | | |
| | | provide('dictObj', dictObj) |
| | | provide('workOrderTypeXT', workOrderTypeXT) |
| | | |
| | | // 获取列表 |
| | | async function getList() { |
| | |
| | | getDictionaryByCode('workOrderType,skilledUavType,technicalStrength').then(res => { |
| | | dictObj.value = res.data.data |
| | | }) |
| | | getDictListApi('task_inspection_type').then(res => { |
| | | workOrderTypeXT.value = normalizeTaskTypeOptions(res.data.data || []) |
| | | }) |
| | | } |
| | | |
| | | // 获取飞手列表 |
| | |
| | | <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" |
| | |
| | | |
| | | // 初始化表单数据 |
| | | const initForm = () => ({ |
| | | eventName: '', // 事件名称 |
| | | disposeDept: '', // 处置部门 |
| | | disposeUser: '', // 工单处置人 |
| | | }) |
| | |
| | | |
| | | // 校验规则 |
| | | const rules = { |
| | | eventName: fieldRules(true, 50), |
| | | disposeDept: fieldRules(true), |
| | | disposeUser: fieldRules(true), |
| | | } |
| | |
| | | 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, |
| | |
| | | <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="线索编号" /> |
| | |
| | | <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> |
| | |
| | | @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') |
| | |
| | | const currentRow = ref(null) // 当前行数据 |
| | | const distributeDialogRef = ref(null) |
| | | const distributeDialogVisible = ref(false) |
| | | const VideoShow = ref(false) |
| | | const currentVideo = ref({}) |
| | | |
| | | // 分发状态选项 |
| | | const distributeStatusOptions = [ |
| | |
| | | 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) { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 点击视频 |
| | | function videoClick(row) { |
| | | currentVideo.value = row |
| | | VideoShow.value = true |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ row } = {}) { |
| | | currentRow.value = row |
| | |
| | | </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> |
| | |
| | | popper-class="gd-select-popper" |
| | | v-model="searchParams.patrolTaskType" |
| | | :options="workOrderTypeXT" |
| | | :props="taskTypeFilterCascaderProps" |
| | | :props="taskTypeCascaderProps" |
| | | placeholder="请选择" |
| | | clearable |
| | | @change="handleSearch" |
| | |
| | | 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 = () => ({ |
| | |
| | | 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 |
| | |
| | | </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"> |
| | |
| | | <!-- <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"> |
| | |
| | | <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"> |
| | |
| | | </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) |
| | |
| | | // 上传名称 |
| | | const uploadName = ref('') |
| | | |
| | | const dictObj = ref({ |
| | | workOrderType: [], // 巡查任务类型 |
| | | }) |
| | | const workOrderTypeXT = ref([]) |
| | | |
| | | const searchParams = ref(initSearchParams()) // 查询参数 |
| | | const total = ref(0) // 总条数 |
| | |
| | | |
| | | // 获取字典 |
| | | 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 |
| | | } |
| | | |
| | | // 将字节大小转换为可读格式的函数 |
| | |
| | | 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 { |
| | |
| | | if (valid) { |
| | | submitting.value = true |
| | | try { |
| | | await fjSubmitApi({ ...editParams.value }) |
| | | await fjSubmitApi(editParams.value) |
| | | ElMessage.success('保存成功') |
| | | isShowEditView.value = false |
| | | getList() |
| | |
| | | popper-class="gd-select-popper" |
| | | v-model="row.patrolTaskType" |
| | | :options="workOrderTypeXT" |
| | | :props="taskTypeCascaderProps" |
| | | :props="{...taskTypeCascaderProps,multiple:true}" |
| | | placeholder="请选择" |
| | | collapse-tags |
| | | @change="handlePatrolTaskTypeChange($index, $event)" |
| | |
| | | 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] || []" |
| | |
| | | import { airlineListApi, algorithmGroupedApi } from '@/api/zkxt' |
| | | import { |
| | | getTaskTypeLabel, |
| | | getTaskTypeLeafValues, |
| | | taskTypeCascaderProps, |
| | | toTaskTypeCascaderValue, |
| | | toTaskTypeRequestValue, |
| | | } from '../taskTypeOptions' |
| | | |
| | | // 初始化任务行 |
| | |
| | | // 将工单类型赋值给所有子任务行 |
| | | 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 { |
| | |
| | | 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) |
| | |
| | | |
| | | // 获取飞手列表 |
| | | async function getFlyerList(index, skilledTaskType) { |
| | | const leafValues = getTaskTypeLeafValues(skilledTaskType) |
| | | if (!leafValues.length) { |
| | | if (!skilledTaskType.length) { |
| | | flyerListMap.value[index] = [] |
| | | return |
| | | } |
| | |
| | | const res = await gdFlyerPageApi({ |
| | | size: 999, |
| | | current: 1, |
| | | skilledTaskType: leafValues.join(','), |
| | | skilledTaskType: skilledTaskType.map(item => item[1])?.join?.(','), |
| | | // longitude: obj.longitude || '', |
| | | // latitude: obj.latitude || '', |
| | | }) |
| | |
| | | function isTaskRowFilled(task) { |
| | | return !!( |
| | | task.patrolTaskName || |
| | | hasTaskTypeValue(task.patrolTaskType) || |
| | | task.executeTime || |
| | | task.patrolRouteUrl || |
| | | task.deviceFlyerId || |
| | |
| | | ) |
| | | } |
| | | |
| | | function hasTaskTypeValue(value) { |
| | | return !!toTaskTypeRequestValue(value).length |
| | | } |
| | | |
| | | // 提交新增 |
| | | async function handleSubmit() { |
| | |
| | | // 已填写的行需要校验完整性 |
| | | if (!task.patrolTaskName) { |
| | | ElMessage.warning(`第${i + 1}行: 请输入巡查任务名称`) |
| | | return |
| | | } |
| | | if (!hasTaskTypeValue(task.patrolTaskType)) { |
| | | ElMessage.warning(`第${i + 1}行: 请选择巡查任务类型`) |
| | | return |
| | | } |
| | | if (!task.executeTime) { |
| | |
| | | // 只提交已填写的任务行 |
| | | const dataList = filledTasks.map(item => ({ |
| | | ...item, |
| | | patrolTaskType: toTaskTypeRequestValue(item.patrolTaskType), |
| | | workOrderId: selectedWorkOrderId.value, |
| | | id: undefined, |
| | | })) |
| | |
| | | // 编辑/查看模式 - 加载已有数据 |
| | | 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) { |
| | |
| | | 任务成果({{ 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> |
| | |
| | | popper-class="gd-select-popper" |
| | | v-model="formData.patrolTaskType" |
| | | :options="workOrderTypeXT" |
| | | :props="taskTypeCascaderProps" |
| | | :props="{ ...taskTypeCascaderProps, multiple: true }" |
| | | placeholder="请选择" |
| | | collapse-tags |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | |
| | | 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 = () => ({ |
| | |
| | | children: (group.algorithms || []).map(alg => ({ |
| | | id: alg.id, |
| | | name: alg.name, |
| | | })) |
| | | })), |
| | | })) |
| | | } catch (e) { |
| | | console.error('获取算法列表失败', e) |
| | |
| | | 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 = { |
| | |
| | | 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') |
| | |
| | | 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 { |
| | | } |
| | | } |
| | |
| | | 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: { |
| | |
| | | |
| | | 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) |
| | |
| | | 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() |
| | |
| | | let VideoShow = ref(false) |
| | | let currentVideo = ref({}) |
| | | function videoClick(row) { |
| | | VideoShow.value = true |
| | | currentVideo.value = row |
| | | VideoShow.value = true |
| | | } |
| | | |
| | | // 初始化地图实例 |
| | |
| | | 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%; |
| | |
| | | .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) { |
| | |
| | | </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"> |
| | |
| | | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 新增/编辑/查看 弹框 |
| | | function openForm (mode, row) { |
| | | function openForm(mode, row) { |
| | | dialogVisible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode, row }) |
| | |
| | | |
| | | // 状态: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' |
| | |
| | | } |
| | | |
| | | // 勾选值设置 |
| | | 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 |
| | | }) |
| | |
| | | }) |
| | | } |
| | | |
| | | function refusalAccept (row) { |
| | | function refusalAccept(row) { |
| | | dialogVisible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode: 'edit', row }) |
| | |
| | | } |
| | | |
| | | // 获取两个tab的总条数 |
| | | async function getTabTotals () { |
| | | async function getTabTotals() { |
| | | const allRes = await gdPatrolTaskPageApi({ |
| | | current: 1, |
| | | size: 1, |
| | |
| | | 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> |
| | |
| | | </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"> |
| | |
| | | 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> |
| | |
| | | </div> |
| | | <el-table |
| | | class="separateTable" |
| | | :empty-text=" |
| | | pointList.length ? '暂无数据' : '请先在地图上绘制工单范围' |
| | | " |
| | | :empty-text="pointList.length ? '暂无数据' : '请先在地图上绘制工单范围'" |
| | | ref="deviceTableRef" |
| | | :data="pointList.length ? deviceList : []" |
| | | row-key="id" |
| | |
| | | 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') |
| | |
| | | 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, |
| | |
| | | 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') |
| | |
| | | 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] |
| | | } |
| | | |
| | |
| | | </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> |
| | | |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | .content { |
| | | display: flex; |
| | |
| | | </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"> |
| | |
| | | |
| | | <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> |
| | |
| | | </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> |
| | | |
| | |
| | | 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') |
| | |
| | | provide('workOrderTypeXT', workOrderTypeXT) |
| | | |
| | | // 获取列表 |
| | | async function getList () { |
| | | async function getList() { |
| | | const range = dateRangeFormat(dateRange.value) |
| | | loading.value = true |
| | | try { |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 查询 |
| | | 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 }) |
| | |
| | | // 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' |
| | |
| | | } |
| | | |
| | | // 删除 |
| | | async function handleDelete (row) { |
| | | async function handleDelete(row) { |
| | | const tips = row ? '该条' : '选中的项' |
| | | await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', { |
| | | type: 'warning', |
| | |
| | | } |
| | | |
| | | // 勾选值设置 |
| | | 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 |
| | | }) |
| | |
| | | } |
| | | |
| | | // 获取两个tab的总条数 |
| | | async function getTabTotals () { |
| | | async function getTabTotals() { |
| | | // 获取全部tab的总数 |
| | | const allRes = await gdWorkOrderPageApi({ |
| | | current: 1, |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | </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 }"> |
| | |
| | | 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' |
| | | |
| | |
| | | const initSearchParams = () => ({ |
| | | current: 1, |
| | | size: 10, |
| | | patrolTaskIds: [] |
| | | patrolTaskIds: [], |
| | | attachmentType: '' |
| | | }) |
| | | |
| | | const searchParams = ref(initSearchParams()) |
| | |
| | | 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) |
| | | // 获取巡查任务列表 |
| | |
| | | ? 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 |
| | |
| | | let VideoShow = ref(false) |
| | | let currentVideo = ref({}) |
| | | function videoClick(row) { |
| | | currentVideo.value = row |
| | | VideoShow.value = true |
| | | currentVideo.value = row |
| | | } |
| | | |
| | | // 监听 workOrderId 变化 |
| | |
| | | ) |
| | | } |
| | | |
| | | 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 => { |
| | |
| | | .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, |
| | | } |
| | |
| | | { |
| | | "name": "my-monorepo", |
| | | "private": true, |
| | | "packageManager": "pnpm@9.0.0", |
| | | "packageManager": "pnpm@9.15.9", |
| | | "version": "1.0.0", |
| | | "description": "", |
| | | "main": "index.js", |
| | |
| | | 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 |
| | | }) |
| | | } |
| | |
| | | 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 |
| | |
| | | '@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 |
| | |
| | | '@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==} |
| | |
| | | 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: |
| | |
| | | |
| | | '@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: |
| | |
| | | '@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 |
| | |
| | | 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 |
| | |
| | | { |
| | | "name": "work-app", |
| | | "name": "@ztzf/work-app", |
| | | "type": "module", |
| | | "version": "1.6.0", |
| | | "description": "吉安低空", |
| | |
| | | "@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", |
| | |
| | | "@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", |
| | |
| | | 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', |
| | |
| | | }, |
| | | /* 模块配置 */ |
| | | "modules" : { |
| | | "Share" : {} |
| | | "Share" : {}, |
| | | "Geolocation" : {}, |
| | | "VideoPlayer" : {}, |
| | | "Camera" : {} |
| | | }, |
| | | /* 应用发布信息 */ |
| | | "distribute" : { |
| | |
| | | "appid" : "gh_f2fbbd1d5d2a", |
| | | "UniversalLinks" : "" |
| | | } |
| | | }, |
| | | "geolocation" : { |
| | | "system" : { |
| | | "__platform__" : [ "android" ] |
| | | } |
| | | } |
| | | }, |
| | | "icons" : { |
| | |
| | | "router" : { |
| | | "mode" : "hash", |
| | | "base" : "/work-app/" |
| | | }, |
| | | "sdkConfigs" : { |
| | | "maps" : {} |
| | | } |
| | | }, |
| | | "locale" : "zh-Hans", |
| | |
| | | <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> |
| | |
| | | </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> |
| | | |
| | |
| | | |
| | | <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([]) |
| | |
| | | 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 |
| | | |
| | |
| | | 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 |
| | |
| | | |
| | | const detailHandle = (val) => { |
| | | uni.navigateTo({ |
| | | url: `/subPackages/workDetail/index?id=${val.id}`, |
| | | url: `/subPackages/workDetail/index?id=${val.id}` + (val.aiImg ? `&aiImg=${val.aiImg}` : ''), |
| | | }) |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | font-weight: 400; |
| | | font-size: 24rpx; |
| | | color: #FFFFFF; |
| | | padding-right: 14rpx; |
| | | margin-left: 12rpx; |
| | | white-space: nowrap; |
| | | |
| | | } |
| | | } |
| | |
| | | color: #999; |
| | | width: 100%; |
| | | } |
| | | |
| | | .ai-image-canvas { |
| | | position: fixed; |
| | | left: -9999px; |
| | | top: -9999px; |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <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> |
| | |
| | | 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 = () => { |
| | |
| | | const imageArr = [] |
| | | const detail = workDetailData.value |
| | | if (detail.eventImageUrl) { |
| | | const smallUrl = detail.eventImageUrl |
| | | imageArr.push(smallUrl) |
| | | imageArr.push(detail.aiImg || detail.eventImageUrl) |
| | | } |
| | | |
| | | return imageArr |
| | | }) |
| | | // 跳转地图 |