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