| | |
| | | <div class="detail-container" style="display: flex"> |
| | | <div class="processBox"> |
| | | <div class="detail-title">巡查需求记录</div> |
| | | <div class="detail-left" v-if="dialogMode !== 'add' && processList.length"> |
| | | <div class="detail-left" v-if="dialogMode !== 'add'"> |
| | | <el-timeline class="gd-timeline"> |
| | | <el-timeline-item |
| | | v-for="(activity, index) in processList" |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="label">巡查任务类型</div> |
| | | <div class="val">{{ getDictLabel(formData.patrolTaskType, dictObj.workOrderType) }}</div> |
| | | <div class="val">{{ getTaskTypeLabel(formData.patrolTaskType, workOrderTypeXT) }}</div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="label">任务执行时间</div> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="label">推荐飞手</div> |
| | | <div class="val">{{ formData.recommendFlyerName }}</div> |
| | | <div class="val">{{ getFlyerName(formData.deviceFlyerId) }}</div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="label">选择设备</div> |
| | |
| | | <div class="label">巡查任务描述</div> |
| | | <div class="val">{{ formData.taskDesc }}</div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="label">算法</div> |
| | | <div class="val">{{ getAlgorithmNames(formData.algorithmIds) }}</div> |
| | | </el-col> |
| | | </el-row> |
| | | <template v-if="['6', '7', '8'].includes(taskStatus)"> |
| | | <div class="detail-title" :style="{ marginTop: pxToRem(10) }"> |
| | | 任务成果({{ 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.resultUrl" |
| | | :src="item.resultUrl" |
| | | :key="item.thumbnail" |
| | | v-if="item.attachmentType === 1 || item.attachmentType === 2" |
| | | :src="item.thumbnail" |
| | | :preview-src-list="[item.resultUrl]" |
| | | fit="cover" |
| | | preview-teleported |
| | | @error="handleThumbnailError(item)" |
| | | /> |
| | | <div class="video-btn" v-if="item.attachmentType === 3" @click="videoClick(item)"> |
| | | <el-icon :size="30" color="#fff"> |
| | | <VideoPlay /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="巡查任务类型" prop="patrolTaskType"> |
| | | <el-select |
| | | <el-cascader |
| | | class="gd-select" |
| | | popper-class="gd-select-popper" |
| | | v-model="formData.patrolTaskType" |
| | | :options="workOrderTypeXT" |
| | | :props="{ ...taskTypeCascaderProps, multiple: true }" |
| | | placeholder="请选择" |
| | | collapse-tags |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="item in dictObj.workOrderType" |
| | | :key="item.dictKey" |
| | | :label="item.dictValue" |
| | | :value="item.dictKey" |
| | | /> |
| | | </el-select> |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="推荐飞手" prop="recommendFlyerName"> |
| | | <el-form-item label="推荐飞手" prop="deviceFlyerId"> |
| | | <el-select |
| | | class="gd-select" |
| | | popper-class="gd-select-popper" |
| | | v-model="formData.recommendFlyerName" |
| | | v-model="formData.deviceFlyerId" |
| | | placeholder="请选择" |
| | | filterable |
| | | clearable |
| | | > |
| | | <el-option v-for="item in flyerList" :key="item.id" :label="item.flyerName" :value="item.flyerName" /> |
| | | <el-option v-for="item in flyerList" :key="item.id" :label="item.flyerName" :value="item.flyerId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | <span>{{ getWorkOrderName(formData.workOrderId) }}</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <!-- 算法下拉 --> |
| | | <el-col :span="12"> |
| | | <el-form-item label="算法" prop="algorithmIds"> |
| | | <el-tree-select |
| | | class="gd-select" |
| | | popper-class="gd-tree-select-popper" |
| | | v-model="formData.algorithmIds" |
| | | :data="algorithmTreeData" |
| | | :props="{ label: 'name', children: 'children', value: 'id' }" |
| | | node-key="id" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | check-strictly |
| | | filterable |
| | | clearable |
| | | placeholder="请选择" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="巡查任务描述" prop="taskDesc"> |
| | | <el-input |
| | | class="gd-input" |
| | | class="gd-input-text" |
| | | type="textarea" |
| | | :rows="4" |
| | | v-model="formData.taskDesc" |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <template #footer v-if="detailLoaded"> |
| | | <el-button v-if="['1', '4', '7'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5"> |
| | | {{ gdStatusObj[taskStatus].reason }} |
| | | </el-button> |
| | |
| | | @success="rejectSuccess" |
| | | /> |
| | | </el-dialog> |
| | | |
| | | <VideoPlayDialog |
| | | ref="videoPlayDialogRef" |
| | | v-if="VideoShow" |
| | | v-model="VideoShow" |
| | | :playUrl="currentVideo.resultUrl" |
| | | ></VideoPlayDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, onMounted } from 'vue' |
| | | import { computed, ref, onMounted, inject } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { fieldRules, flyVisual, getDictLabel } from '@ztzf/utils' |
| | | import { fieldRules, flyVisual, geomAnalysis, appendAiImages } from '@ztzf/utils' |
| | | import { |
| | | gdPatrolTaskRepublish, |
| | | gdFlyerPageApi, |
| | | gdPatrolTaskAuditApi, |
| | | gdPatrolTaskDetailApi, |
| | | } from './inspectionRequestApi' |
| | | import { gdWorkOrderFlowListApi, gdWorkOrderFlowPatrolListApi, gdWorkOrderPageApi } 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 { queryAirById } from '@/api/zkxt' |
| | | 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 } from '../taskTypeOptions' |
| | | |
| | | // 初始化表单数据 |
| | | const initForm = () => ({ |
| | | id: null, |
| | | patrolTaskName: '', |
| | | patrolTaskType: '', |
| | | patrolTaskType: [], |
| | | executeTime: '', |
| | | patrolRouteUrl: '', |
| | | recommendFlyerName: '', |
| | | deviceFlyerId: null, |
| | | deviceId: null, |
| | | algorithmIds: [], |
| | | taskDesc: '', |
| | | workOrderId: null, |
| | | }) |
| | |
| | | const visible = defineModel() |
| | | const dialogMode = ref('view') |
| | | const submitting = ref(false) |
| | | const detailLoaded = ref(false) |
| | | const dialogReadonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑', view: '查看' }) |
| | | const taskStatus = computed(() => formData.value.taskStatus) |
| | | const refuseOrderDialogRef = ref(null) |
| | | |
| | | // 下拉选项 |
| | | const flyerList = ref([]) |
| | | const deviceList = ref([]) |
| | | const workOrderList = ref([]) |
| | | const routeOptions = inject('routeOptions') |
| | | const getAirName = inject('getAirName') |
| | | const workOrderDetail = ref(null) // 工单详情 |
| | | const routeOptions = ref([]) // 航线选项(根据工单范围获取) |
| | | const getAirName = id => { |
| | | const item = routeOptions.value.find(item => item.id === id) |
| | | return item ? item.name : id |
| | | } |
| | | |
| | | const algorithmTreeData = ref([]) // 算法树形数据 |
| | | const workOrderTypeXT = inject('workOrderTypeXT', ref([])) |
| | | |
| | | // 获取算法分组数据 |
| | | async function getAlgorithmList() { |
| | | try { |
| | | const res = await algorithmGroupedApi() |
| | | const list = res?.data?.data ?? [] |
| | | algorithmTreeData.value = list.map(group => ({ |
| | | id: 'group_' + group.type, |
| | | name: group.type, |
| | | disabled: true, |
| | | children: (group.algorithms || []).map(alg => ({ |
| | | id: alg.id, |
| | | name: alg.name, |
| | | })), |
| | | })) |
| | | } catch (e) { |
| | | console.error('获取算法列表失败', e) |
| | | } |
| | | } |
| | | |
| | | // 根据 algorithmIds 数组获取算法名称,逗号分割回显 |
| | | 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(', ') |
| | | } |
| | | |
| | | const gdStatusObj = { |
| | | '0': { reason: '拒绝原因', operationType: '2', title: '拒绝签收' }, |
| | |
| | | patrolTaskType: fieldRules(true), |
| | | executeTime: fieldRules(true), |
| | | patrolRouteUrl: fieldRules(true), |
| | | recommendFlyerName: fieldRules(true), |
| | | deviceFlyerId: fieldRules(true), |
| | | deviceId: fieldRules(true), |
| | | } |
| | | |
| | |
| | | return item ? item.nickname : id |
| | | } |
| | | |
| | | // 获取工单名称 |
| | | function getWorkOrderName(id) { |
| | | const item = workOrderList.value.find(item => item.id === id) |
| | | return item ? item.workOrderName : id |
| | | // 获取飞手名称 |
| | | function getFlyerName(id) { |
| | | const item = flyerList.value.find(item => item.flyerId === id) |
| | | return item ? item.flyerName : id |
| | | } |
| | | |
| | | // 获取工单列表 |
| | | async function getWorkOrderList() { |
| | | const res = await gdWorkOrderPageApi({ size: 999 }) |
| | | workOrderList.value = res?.data?.data?.records ?? [] |
| | | // 获取工单名称 |
| | | function getWorkOrderName(id) { |
| | | if (workOrderDetail.value?.id === id) return workOrderDetail.value.workOrderName |
| | | return id |
| | | } |
| | | |
| | | // 获取飞手列表 |
| | |
| | | } |
| | | |
| | | let taskResultList = ref([]) |
| | | // 缩略图加载失败时展示原图 |
| | | function handleThumbnailError(item) { |
| | | if (item.thumbnail !== item.resultUrl) { |
| | | item.thumbnail = item.resultUrl |
| | | } |
| | | } |
| | | |
| | | // 获取成果列表 |
| | | async function getTaskResultList() { |
| | | if (!formData.value?.id) return |
| | | try { |
| | | const res = await gdTaskResultListApi({ patrolTaskId: formData.value.id }) |
| | | taskResultList.value = res?.data?.data ?? [] |
| | | taskResultList.value = (res?.data?.data ?? []).map(item => ({ |
| | | ...item, |
| | | thumbnail: item.resultUrl.replace(/(\.[^./?]+)(\?.*)?$/, '_thumbnail$1$2'), |
| | | })) |
| | | taskResultList.value.forEach(item => { |
| | | if (item.attachmentType === 2 && item.geojson !== '[]' && item.geojson?.length) { |
| | | appendAiImages(item) |
| | | } |
| | | }) |
| | | } finally { |
| | | } |
| | | } |
| | | import { ArrowLineMaterialProperty } from '@/utils/cesium/Material' |
| | | import OrderStepBar from '@/views/orderView/orderManage/orderManage/OrderStepBar.vue' |
| | | import VideoPlayDialog from '@/components/VideoPlayDialog.vue' |
| | | let arrowLineMaterialProperty = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | |
| | | }) |
| | | // 获取航线详情 |
| | | function getAirDetails() { |
| | | console.log(formData.value) |
| | | const dockHeight = formData.value.height |
| | | queryAirById(formData.value.patrolRouteUrl).then(res => { |
| | | const list = res.data.data.airlineWaypoints |
| | | const { airlineWaypoints, airlineSetting } = res.data.data |
| | | // 使用空值合并运算符 ?? 或 逻辑或 || 兜底 |
| | | const { globalAirlineHeight = 0, airlineHeightMode } = airlineSetting || {} |
| | | const relative = airlineHeightMode === 'relativeToStartPoint' |
| | | const list = airlineWaypoints |
| | | if (!list.length) return mapRef.value?.flyBoundary() |
| | | const result = list.map(item => [Number(item.longitude), Number(item.latitude)]).flat() |
| | | const result = list.map(item => { |
| | | let height = item?.globalHeight || globalAirlineHeight |
| | | if (relative) { |
| | | height = height + Number(dockHeight) |
| | | } |
| | | return [Number(item.longitude), Number(item.latitude), height || 0] |
| | | }) |
| | | viewer.entities.add({ |
| | | polyline: { |
| | | positions: Cesium.Cartesian3.fromDegreesArray(result), |
| | | clampToGround: true, |
| | | positions: Cesium.Cartesian3.fromDegreesArrayHeights(result.flat()), |
| | | width: 4, |
| | | material: arrowLineMaterialProperty, |
| | | }, |
| | | }) |
| | | // 渲染起点(蓝色) |
| | | const startPoint = list[0] |
| | | const startPoint = result[0] |
| | | viewer.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees( |
| | | Number(startPoint.longitude), |
| | | Number(startPoint.latitude), |
| | | startPoint.height || 0 |
| | | ), |
| | | position: Cesium.Cartesian3.fromDegrees(Number(startPoint[0]), Number(startPoint[1]), startPoint[2]), |
| | | point: { |
| | | pixelSize: 12, |
| | | color: Cesium.Color.BLUE, |
| | | outlineColor: Cesium.Color.WHITE, |
| | | outlineWidth: 2, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | // 渲染终点(红色) |
| | | const endPoint = list[list.length - 1] |
| | | const endPoint = result[result.length - 1] |
| | | viewer.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees( |
| | | Number(endPoint.longitude), |
| | | Number(endPoint.latitude), |
| | | endPoint.height || 0 |
| | | ), |
| | | position: Cesium.Cartesian3.fromDegrees(Number(endPoint[0]), Number(endPoint[1]), endPoint[2]), |
| | | point: { |
| | | pixelSize: 12, |
| | | color: Cesium.Color.RED, |
| | | outlineColor: Cesium.Color.WHITE, |
| | | outlineWidth: 2, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | flyVisual({ |
| | | positionsData: list.map(i => [Number(i.longitude), Number(i.latitude), i.height || 0]), |
| | | viewer, |
| | | multiple: 6, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode = 'view', row } = {}) { |
| | | detailLoaded.value = false |
| | | dialogMode.value = mode |
| | | |
| | | const res = await gdPatrolTaskDetailApi({ id: row.id }) |
| | | formData.value = { ...initForm(), ...res.data.data } |
| | | ;['6', '7', '8'].includes(row.taskStatus) && await getTaskResultList() |
| | | // 确保 algorithmIds 是数组 |
| | | if (formData.value.algorithmIds && typeof formData.value.algorithmIds === 'string') { |
| | | formData.value.algorithmIds = formData.value.algorithmIds.split(',').filter(Boolean) |
| | | } else if (!formData.value.algorithmIds) { |
| | | formData.value.algorithmIds = [] |
| | | } |
| | | detailLoaded.value = true |
| | | // 获取算法列表 |
| | | getAlgorithmList() |
| | | // 获取工单详情并根据geom范围获取航线列表 |
| | | await getWorkOrderDetail(formData.value.workOrderId) |
| | | ;['6', '7', '8'].includes(row.taskStatus) && (await getTaskResultList()) |
| | | loadList() |
| | | initMap() |
| | | getAirDetails() |
| | | } |
| | | |
| | | // 获取工单详情 |
| | | async function getWorkOrderDetail(workOrderId) { |
| | | if (!workOrderId) return |
| | | try { |
| | | const res = await gdWorkOrderDetailApi({ id: workOrderId }) |
| | | workOrderDetail.value = res?.data?.data ?? null |
| | | // 根据工单geom范围获取航线列表 |
| | | const geom = workOrderDetail.value?.geom |
| | | if (!geom) return |
| | | const coordinates = geomAnalysis(geom) |
| | | if (!coordinates || !coordinates.length) return |
| | | const airRes = await airlineListApi(coordinates) |
| | | routeOptions.value = (airRes.data.data || []).filter(item => item) |
| | | } catch (e) { |
| | | console.error('获取工单详情或航线列表失败', e) |
| | | } |
| | | } |
| | | |
| | | // 点击视频 |
| | | let VideoShow = ref(false) |
| | | let currentVideo = ref({}) |
| | | function videoClick(row) { |
| | | currentVideo.value = row |
| | | VideoShow.value = true |
| | | } |
| | | |
| | | // 初始化地图实例 |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getWorkOrderList() |
| | | getFlyerList() |
| | | getDeviceList() |
| | | }) |
| | |
| | | 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%; |
| | | height: 100%; |
| | | } |
| | | .video-btn { |
| | | width: 100%; |
| | | height: 100%; |
| | | 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) { |
| | | font-size: 12px; |