| | |
| | | :title="titleEnum[dialogMode]" |
| | | @closed="visible = false" |
| | | destroy-on-close |
| | | width="60%" |
| | | width="90%" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="gd-dialog-form"> |
| | | <div> |
| | | <!-- 关联工单选择 --> |
| | | <div class="work-order-section"> |
| | | <div class="detail-title" :style="{ marginBottom: 0, marginRight: pxToRem(10) }">选择工单</div> |
| | |
| | | clearable |
| | | filterable |
| | | :disabled="dialogMode !== 'add'" |
| | | @change="handleWorkOrderChange" |
| | | > |
| | | <el-option v-for="item in workOrderList" :key="item.id" :label="item.workOrderName" :value="item.id" /> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <!-- 工单信息展示 --> |
| | | <div class="work-order-info" v-if="selectedWorkOrder"> |
| | | <div class="work-order-info"> |
| | | <el-row class="detail-row-view"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="label">工单类型</span> |
| | | <span class="val">{{ getDictLabel(selectedWorkOrder.workOrderType, dictObj.workOrderType) }}</span> |
| | | <span class="val" v-if="selectedWorkOrder"> |
| | | {{ getTaskTypeLabel(selectedWorkOrder.workOrderType, workOrderTypeXT) }} |
| | | </span> |
| | | <span class="val grayText" v-else>选择工单后自动带出</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="label">执行时间范围</span> |
| | | <span class="val">{{ selectedWorkOrder.executeStartTime }} ~ {{ selectedWorkOrder.executeEndTime }}</span> |
| | | <span class="val" v-if="selectedWorkOrder"> |
| | | {{ selectedWorkOrder.executeStartTime }} ~ {{ selectedWorkOrder.executeEndTime }} |
| | | </span> |
| | | <span class="val grayText" v-else>选择工单后自动带出</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="label">设备需求</span> |
| | | <span class="val">{{ getDictLabel(selectedWorkOrder.deviceLoadDemand, dictObj.deviceLoadDemand) }}</span> |
| | | <span class="val" v-if="selectedWorkOrder"> |
| | | {{ getDictLabel(selectedWorkOrder.deviceLoadDemand, dictObj.deviceLoadDemand) }} |
| | | </span> |
| | | <span class="val grayText" v-else>选择工单后自动带出</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="label">工单备注</span> |
| | | <span class="val">{{ selectedWorkOrder.remark }}</span> |
| | | <span class="val" v-if="selectedWorkOrder">{{ selectedWorkOrder.remark }}</span> |
| | | <span class="val grayText" v-else>选择工单后自动带出</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 子任务表格表单 --> |
| | | <div class="task-table-section" v-if="selectedWorkOrderId"> |
| | | <div |
| | | class="task-table-section" |
| | | :class="{ 'disabled-section': !selectedWorkOrderId }" |
| | | @click="handleTaskSectionClick" |
| | | > |
| | | <div class="detail-title" :style="{ marginTop: pxToRem(10) }">添加子任务</div> |
| | | <div class="task-table-container"> |
| | | <el-table :data="taskList"> |
| | | <el-table-column width="50" label=""> |
| | | <template v-slot="{ $index }">{{ $index + 1 }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="patrolTaskName" label="巡查任务名称" min-width="120"> |
| | | <el-table-column prop="patrolTaskName" min-width="100"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 巡查任务名称 |
| | | </template> |
| | | <template v-slot="{ row }"> |
| | | <el-input |
| | | class="gd-input" |
| | |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="patrolTaskType" label="巡查任务类型" min-width="140"> |
| | | <el-table-column prop="patrolTaskType" min-width="120"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 巡查任务类型 |
| | | </template> |
| | | <template v-slot="{ row, $index }"> |
| | | <el-select |
| | | <el-cascader |
| | | class="gd-select" |
| | | popper-class="gd-select-popper" |
| | | v-model="row.patrolTaskType" |
| | | :options="workOrderTypeXT" |
| | | :props="{...taskTypeCascaderProps,multiple:true}" |
| | | placeholder="请选择" |
| | | :disabled="dialogReadonly" |
| | | collapse-tags |
| | | @change="handlePatrolTaskTypeChange($index, $event)" |
| | | > |
| | | <el-option |
| | | v-for="item in dictObj.patrolTaskType" |
| | | :key="item.dictKey" |
| | | :label="item.dictValue" |
| | | :value="item.dictKey" |
| | | /> |
| | | </el-select> |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="executeTime" label="任务执行时间" min-width="180"> |
| | | <el-table-column prop="executeTime" width="200"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 任务执行时间 |
| | | </template> |
| | | <template v-slot="{ row }"> |
| | | <!-- gd-date-picker-popper--> |
| | | <el-date-picker |
| | | :style="{width: pxToRem(165)}" |
| | | class="gd-date-picker" |
| | | popper-class="gd-date-picker-popper" |
| | | popper-class="" |
| | | v-model="row.executeTime" |
| | | type="datetime" |
| | | placeholder="请选择" |
| | |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="patrolRouteUrl" label="巡查任务航线" min-width="120"> |
| | | <el-table-column prop="patrolRouteUrl" min-width="100"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 巡查任务航线 |
| | | </template> |
| | | <template v-slot="{ row }"> |
| | | <el-select |
| | | class="gd-select" |
| | |
| | | v-model="row.patrolRouteUrl" |
| | | placeholder="请选择" |
| | | :disabled="dialogReadonly" |
| | | :loading="routeLoading" |
| | | no-data-text="暂无航线" |
| | | > |
| | | <el-option v-for="item in routeOptions" :key="item.id" :label="item.name" :value="item.id" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="recommendFlyerName" label="推荐飞手" min-width="120"> |
| | | <el-table-column prop="deviceFlyerId" min-width="100"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 推荐飞手 |
| | | </template> |
| | | <template v-slot="{ row, $index }"> |
| | | <el-select |
| | | no-data-text="暂无推荐飞手" |
| | | class="gd-select" |
| | | popper-class="gd-select-popper" |
| | | v-model="row.recommendFlyerName" |
| | | v-model="row.deviceFlyerId" |
| | | placeholder="请选择" |
| | | filterable |
| | | :disabled="dialogReadonly || !taskList[$index].patrolTaskType" |
| | | > |
| | | <el-option |
| | | v-for="item in flyerListMap[$index] || []" |
| | | :key="item.id" |
| | | :key="item.flyerId" |
| | | :label="item.flyerName" |
| | | :value="item.flyerName" |
| | | /> |
| | | </el-select> |
| | | :value="item.flyerId" |
| | | > |
| | | <template #default> |
| | | <!-- 匹配区域=1 显示红星号 --> |
| | | <span v-if="item.taskTypeMatchFlag === 1" style="color: #f56c6c; margin-right: 4px;">*</span> |
| | | <span>{{ item.flyerName }}</span> |
| | | </template> |
| | | </el-option> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deviceId" label="选择设备" min-width="120"> |
| | | <el-table-column prop="deviceId" min-width="100"> |
| | | <template #header> |
| | | <span class="required-star">*</span> |
| | | 选择设备 |
| | | </template> |
| | | <template v-slot="{ row }"> |
| | | <el-select |
| | | class="gd-select" |
| | |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="taskDesc" label="巡查任务描述" min-width="120"> |
| | | <el-table-column prop="algorithmIds" min-width="160"> |
| | | <template #header> |
| | | 算法 |
| | | </template> |
| | | <template v-slot="{ row }"> |
| | | <el-tree-select |
| | | class="gd-select" |
| | | popper-class="gd-tree-select-popper" |
| | | v-model="row.algorithmIds" |
| | | :data="algorithmTreeData" |
| | | :props="{ label: 'name', children: 'children', value: 'id' }" |
| | | node-key="id" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | check-strictly |
| | | filterable |
| | | placeholder="请选择" |
| | | :disabled="dialogReadonly" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | |
| | | <el-table-column prop="taskDesc" label="巡查任务描述"> |
| | | <template v-slot="{ row }"> |
| | | <el-input class="gd-input" v-model="row.taskDesc" placeholder="请输入" :disabled="dialogReadonly" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="80" fixed="right" v-if="dialogMode === 'add'"> |
| | | <el-table-column label="操作" width="90" fixed="right" v-if="dialogMode === 'add'"> |
| | | <template v-slot="{ $index }"> |
| | | <el-button type="primary" :icon="Plus" circle size="small" @click="addTaskRow" /> |
| | | <el-button |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, onMounted } from 'vue' |
| | | import { computed, ref, onMounted, inject } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { Plus, Delete } from '@element-plus/icons-vue' |
| | | import { getDictLabel } from '@ztzf/utils' |
| | | import { geomAnalysis, getDictLabel } from '@ztzf/utils' |
| | | |
| | | import { gdPatrolTaskSaveApi, gdFlyerPageApi, gdPatrolTaskAuditApi } from './inspectionRequestApi' |
| | | import { gdWorkOrderPageApi } from '../orderManage/orderManageApi' |
| | | import { gdManageDeviceListApi } from '../orderManage/gdManageDeviceApi' |
| | | import { pxToRem } from '@/utils/rem' |
| | | import { airlineListApi } from '@/api/zkxt' |
| | | import { airlineListApi, algorithmGroupedApi } from '@/api/zkxt' |
| | | import { |
| | | getTaskTypeLabel, |
| | | taskTypeCascaderProps, |
| | | } from '../taskTypeOptions' |
| | | |
| | | // 初始化任务行 |
| | | const initTaskRow = () => ({ |
| | | patrolTaskName: '', |
| | | patrolTaskType: '', |
| | | patrolTaskType: [], |
| | | executeTime: '', |
| | | patrolRouteUrl: '', |
| | | recommendFlyerName: '', |
| | | deviceFlyerId: '', |
| | | deviceId: null, |
| | | algorithmIds: [], |
| | | taskDesc: '', |
| | | }) |
| | | // todo 时间线 |
| | | const dictObj = inject('dictObj') |
| | | |
| | | const emit = defineEmits(['success']) |
| | | const visible = defineModel() // 弹框显隐 |
| | | const dialogMode = ref('add') // 弹框模式 |
| | | const submitting = ref(false) // 提交中 |
| | | const dialogReadonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' }) |
| | | const titleEnum = ref({ edit: '新增', view: '查看', add: '新增' }) |
| | | |
| | | // 工单相关 |
| | | const workOrderList = ref([]) // 工单列表 |
| | |
| | | // 下拉选项 |
| | | const flyerListMap = ref({}) // 飞手列表,按行索引存储 |
| | | const deviceList = ref([]) // 设备列表 |
| | | const routeOptions = inject('routeOptions') |
| | | const routeOptions = ref([]) // 航线选项(根据工单范围获取) |
| | | const routeLoading = ref(false) // 航线加载中 |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | // 子任务列表 |
| | | const taskList = ref([initTaskRow()]) |
| | |
| | | function handleWorkOrderChange(val) { |
| | | if (val) { |
| | | selectedWorkOrder.value = workOrderList.value.find(item => item.id === val) |
| | | // 根据工单范围获取航线列表 |
| | | getAirListByGeom() |
| | | // 将工单类型赋值给所有子任务行 |
| | | const workOrderType = selectedWorkOrder.value?.workOrderType |
| | | if (workOrderType) { |
| | | taskList.value.forEach((task, index) => { |
| | | taskList.value[index] = { |
| | | ...initTaskRow(), |
| | | patrolTaskType: workOrderType, |
| | | } |
| | | // 获取对应的飞手列表 |
| | | getFlyerList(index, workOrderType) |
| | | }) |
| | | } |
| | | } else { |
| | | selectedWorkOrder.value = null |
| | | routeOptions.value = [] |
| | | } |
| | | } |
| | | |
| | | // 根据工单geom范围获取航线列表 |
| | | function getAirListByGeom() { |
| | | const geom = selectedWorkOrder.value?.geom |
| | | if (!geom) return |
| | | const coordinates = geomAnalysis(geom) |
| | | if (!coordinates || !coordinates.length) return |
| | | routeLoading.value = true |
| | | airlineListApi(coordinates).then(res => { |
| | | routeOptions.value = (res.data.data || []).filter(item => item) |
| | | }).finally(() => { |
| | | routeLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | // 添加任务行 |
| | | function addTaskRow() { |
| | | taskList.value.push(initTaskRow()) |
| | | const newTask = initTaskRow() |
| | | // 如果已选择工单,自动填充工单类型 |
| | | if (selectedWorkOrder.value?.workOrderType) { |
| | | newTask.patrolTaskType = selectedWorkOrder.value.workOrderType |
| | | } |
| | | const newIndex = taskList.value.length |
| | | taskList.value.push(newTask) |
| | | // 获取对应的飞手列表 |
| | | if (newTask.patrolTaskType) { |
| | | getFlyerList(newIndex, newTask.patrolTaskType) |
| | | } |
| | | } |
| | | |
| | | // 删除任务行 |
| | |
| | | } |
| | | } |
| | | |
| | | // 处理任务区域点击事件(未选择工单时提示) |
| | | function handleTaskSectionClick() { |
| | | if (!selectedWorkOrderId.value) { |
| | | ElMessage.warning('请先选择工单') |
| | | } |
| | | } |
| | | |
| | | // 获取工单列表 |
| | | async function getWorkOrderList() { |
| | | const res = await gdWorkOrderPageApi({ size: 999, workOrderStatus: dialogMode.value ==='add' ? '20' : '' }) |
| | | const res = await gdWorkOrderPageApi({ size: 999, workOrderStatus: dialogMode.value === 'add' ? '20' : '' }) |
| | | workOrderList.value = res?.data?.data?.records ?? [] |
| | | } |
| | | |
| | | // 处理巡查任务类型变化 |
| | | async function handlePatrolTaskTypeChange(index, skilledTaskType) { |
| | | // 清空该行已选择的飞手 |
| | | taskList.value[index].recommendFlyerName = '' |
| | | taskList.value[index].deviceFlyerId = '' |
| | | // 获取对应的飞手列表 |
| | | await getFlyerList(index, skilledTaskType) |
| | | } |
| | | |
| | | |
| | | |
| | | import * as turf from '@turf/turf' |
| | | |
| | | function getPolygonCenter(rawData) { |
| | | const coordinates = [rawData.map(p => [p.longitude, p.latitude])]; |
| | | const polygon = turf.polygon(coordinates); |
| | | const center = turf.centroid(polygon); |
| | | const [centerLon, centerLat] = center.geometry.coordinates; |
| | | return {longitude: centerLon, latitude: centerLat}; |
| | | } |
| | | |
| | | |
| | | // 获取飞手列表 |
| | | async function getFlyerList(index, skilledTaskType) { |
| | | if (!skilledTaskType.length) { |
| | | flyerListMap.value[index] = [] |
| | | return |
| | | } |
| | | const obj = getPolygonCenter(geomAnalysis(selectedWorkOrder.value.geom)) |
| | | const res = await gdFlyerPageApi({ |
| | | size: 999, |
| | | skilledTaskType, |
| | | current: 1, |
| | | skilledTaskType: skilledTaskType.map(item => item[1])?.join?.(','), |
| | | // longitude: obj.longitude || '', |
| | | // latitude: obj.latitude || '', |
| | | }) |
| | | flyerListMap.value[index] = res?.data?.data?.records ?? [] |
| | | } |
| | |
| | | // 获取设备列表 |
| | | async function getDeviceList() { |
| | | const res = await gdManageDeviceListApi({ |
| | | deviceIds: selectedWorkOrder.value.recommendDeviceIds, |
| | | deviceIds: selectedWorkOrder.value?.recommendDeviceIds, |
| | | }) |
| | | deviceList.value = res?.data?.data ?? [] |
| | | } |
| | |
| | | watch( |
| | | () => selectedWorkOrderId.value, |
| | | () => { |
| | | handleWorkOrderChange(selectedWorkOrderId.value) |
| | | getDeviceList() |
| | | } |
| | | ) |
| | | |
| | | // 判断一行是否已填写(至少填写了任务名称) |
| | | function isTaskRowFilled(task) { |
| | | return !!( |
| | | task.patrolTaskName || |
| | | task.executeTime || |
| | | task.patrolRouteUrl || |
| | | task.deviceFlyerId || |
| | | task.deviceId |
| | | ) |
| | | } |
| | | |
| | | |
| | | // 提交新增 |
| | | async function handleSubmit() { |
| | |
| | | ElMessage.warning('请选择关联工单') |
| | | return |
| | | } |
| | | if (!taskList.value.length) { |
| | | ElMessage.warning('请添加至少一个子任务') |
| | | // 过滤出已填写的任务行 |
| | | const filledTasks = taskList.value.filter(task => isTaskRowFilled(task)) |
| | | if (!filledTasks.length) { |
| | | ElMessage.warning('请至少填写一个子任务') |
| | | return |
| | | } |
| | | // 校验子任务 |
| | | // 只校验已填写的子任务 |
| | | for (let i = 0; i < taskList.value.length; i++) { |
| | | const task = taskList.value[i] |
| | | // 跳过未填写的行 |
| | | if (!isTaskRowFilled(task)) continue |
| | | // 已填写的行需要校验完整性 |
| | | if (!task.patrolTaskName) { |
| | | ElMessage.warning(`第${i + 1}行: 请输入巡查任务名称`) |
| | | return |
| | | } |
| | | if (!task.patrolTaskType) { |
| | | ElMessage.warning(`第${i + 1}行: 请选择巡查任务类型`) |
| | | return |
| | | } |
| | | if (!task.executeTime) { |
| | |
| | | ElMessage.warning(`第${i + 1}行: 请选择巡查任务航线`) |
| | | return |
| | | } |
| | | if (!task.recommendFlyerName) { |
| | | if (!task.deviceFlyerId) { |
| | | ElMessage.warning(`第${i + 1}行: 请选择推荐飞手`) |
| | | return |
| | | } |
| | |
| | | } |
| | | submitting.value = true |
| | | try { |
| | | const preID = taskList.value[0].id |
| | | const dataList = taskList.value.map(item => ({ |
| | | const preID = filledTasks[0]?.id |
| | | // 只提交已填写的任务行 |
| | | const dataList = filledTasks.map(item => ({ |
| | | ...item, |
| | | workOrderId: selectedWorkOrderId.value, |
| | | id: undefined, |
| | |
| | | // 打开弹框 |
| | | async function open({ mode = 'add', row } = {}) { |
| | | dialogMode.value = mode |
| | | await getWorkOrderList() |
| | | getAlgorithmList() |
| | | |
| | | if (dialogMode.value === 'add') { |
| | | selectedWorkOrderId.value = null |
| | | selectedWorkOrder.value = null |
| | | taskList.value = [initTaskRow()] |
| | | taskList.value = [initTaskRow(), initTaskRow(), initTaskRow(), initTaskRow(), initTaskRow()] |
| | | flyerListMap.value = {} |
| | | } else { |
| | | // 编辑/查看模式 - 加载已有数据 |
| | | selectedWorkOrderId.value = row?.workOrderId |
| | | handleWorkOrderChange(row?.workOrderId) |
| | | taskList.value = [{ ...row }] |
| | | taskList.value = [{ ...row, patrolTaskType: row?.patrolTaskType || [] }] |
| | | flyerListMap.value = {} |
| | | // 如果有巡查类型,加载对应的飞手列表 |
| | | if (row?.patrolTaskType) { |
| | | getFlyerList(0, row.patrolTaskType) |
| | | getFlyerList(0, taskList.value[0].patrolTaskType) |
| | | } |
| | | } |
| | | getWorkOrderList() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | |
| | | }) |
| | | onMounted(() => {}) |
| | | |
| | | defineExpose({ open }) |
| | | </script> |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .required-star { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | :deep(.el-table__header th) { |
| | | font-weight: normal !important; |
| | | } |
| | | |
| | | .grayText { |
| | | color: #9f9f9f !important; |
| | | } |
| | | |
| | | // 禁用区域样式 - 使用 pointer-events: none 禁用所有子元素的点击 |
| | | .disabled-section { |
| | | .task-table-container { |
| | | * { |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | opacity: 0.6; |
| | | cursor: not-allowed; |
| | | } |
| | | </style> |