| src/views/tickets/ticket.vue | ●●●●● patch | view | raw | blame | history | |
| src/views/tickets/ticketComponent/TicketDetailDialog.vue | ●●●●● patch | view | raw | blame | history |
src/views/tickets/ticket.vue
@@ -284,387 +284,23 @@ /> <!-- 工单详情对话框 --> <el-dialog class="custom-dialog" align-center v-model="detailVisible" title="工单详情" width="80%" append-to-body > <div class="detail-container"> <div class="detail-top-title"> <div class="event-title-center event-orderNumber"> {{ currentDetail.orderNumber || '工单编号' }} </div> <div class="event-title-center">{{ currentDetail.orderName || '事件名称' }}</div> </div> <div v-if="totalTime" class="event-total-time">总耗时:{{ totalTime }}</div> <!-- 工单状态流程 --> <div class="custom-steps-container"> <!-- 标题行 --> <div class="steps-titles"> <div v-for="(status, index) in stepStatusList" :key="index" :class="{ 'step-title': true, active: index <= stepStatusList.indexOf(String(currentDetail.status)), }" > {{ mapStatus(status) }} </div> </div> <!-- Element Steps 组件 --> <el-steps :active="getActiveStep() - 1" align-center class="custom-steps"> <el-step v-for="(status, index) in stepStatusList" :key="index"> <template #description> <span class="step-description"> {{ getStepHandler(status) }} </span> <div class="step-description" v-if="getStepTime(status)"> <span class="step-timer"> 耗时:{{ getStepTime(status) }} </span> </div> <div class="step-description"> {{ getStepCreateTime(status) }} </div> </template> </el-step> </el-steps> </div> <div class="PopUpTableScrolls"> <!-- 基本信息表格 --> <el-table :show-header="false" :data="formattedDetailFields" border class="tableCss"> <el-table-column prop="label1" label="基本信息" width="150"> <template #default="{ row }"> <!-- 添加必填星号的标签 --> <span v-if=" currentDetail.status === 0 && (row.label1 === '关联算法' || row.label1 === '工单名称') " class="required-label" > <span class="required-star">*</span>{{ row.label1 }} </span> <span v-else>{{ row.label1 }}</span> </template> </el-table-column> <el-table-column> <template #default="{ row }"> <template v-if=" currentDetail.status === 0 && row.label1 === '工单名称' && hasProcessingBtnPermission() " > <el-input v-model="currentDetail.orderName" placeholder="请输入工单名称" class="required-input" /> </template> <template v-else-if=" currentDetail.status === 0 && row.label1 === '关联算法' && hasProcessingBtnPermission() " > <el-select v-model="currentDetail.aiType" placeholder="请选择关联算法" class="required-input" > <el-option v-for="item in algorithms2" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template> <template v-else>{{ row.value1 }}</template> </template> </el-table-column> <el-table-column prop="label2" label="基本信息" width="150"> <template #default="{ row }"> <!-- 添加必填星号的标签 --> <span v-if="currentDetail.status === 0 && row.label2 === '工单内容'" class="required-label" > <span class="required-star">*</span>{{ row.label2 }} </span> <span v-else>{{ row.label2 }}</span> </template> </el-table-column> <el-table-column> <template #default="{ row }"> <!-- 修改工单类型和工单内容的显示 --> <template v-if=" currentDetail.status === 0 && row.label2 === '工单内容' && hasProcessingBtnPermission() " > <el-input type="textarea" v-model="currentDetail.content" placeholder="请输入工单内容" class="required-input" /> </template> <template v-else>{{ row.value2 }}</template> </template> </el-table-column> </el-table> <!-- 事件处理详情 --> <div v-if="[3, 4].includes(currentDetail.status)" class="form-section"> <div class="section-title"> <!-- 处理中状态显示必填星号 --> <template v-if="currentDetail.status === 3"> <span class="required-label"> <span class="required-star">*</span>事件处理详情 </span> </template> <template v-else> 事件处理详情</template> </div> <!-- 处理中状态显示输入框 --> <template v-if="currentDetail.status === 3 && hasProcessedAndOverBtnPermission()"> <el-input type="textarea" v-model="currentDetail.processingDetail" placeholder="请输入事件处理详情" :rows="4" style="width: 100%; margin-bottom: 10px" /> </template> <!-- 已完成和已完结状态显示只读文本 --> <template v-else> <div class="readonly-processing-detail"> {{ currentDetail.processingDetail }} </div> </template> </div> <!-- 上传图片 --> <div v-if="[3].includes(currentDetail.status)" class="form-section uploadImg"> <div class="section-title" v-if="hasProcessedAndOverBtnPermission()"> <!-- 已完成状态显示必填星号 --> <template v-if="currentDetail.status === 3"> <span class="required-label"> <span class="required-star">*</span>上传图片 </span> </template> <template v-else> 上传图片</template> </div> <el-upload v-if="hasProcessedAndOverBtnPermission()" ref="upload" :action="'#'" :auto-upload="false" list-type="picture-card" :on-change="handleFileChange" :on-remove="handleUploadRemove" :before-upload="beforeUpload" :file-list="currentDetail.photos || []" :limit="1" accept="image/*" class="detail-upload" > <template v-if="!currentDetail.photos || currentDetail.photos.length < 1"> <!-- <i class="el-icon-plus">+</i> --> <div class="el-icon-plus"> <span>+</span> </div> </template> </el-upload> <div class="el-upload__tip" v-if="hasProcessedAndOverBtnPermission()"> (上传照片即可完结工单,只能上传jpg、jpeg、png照片,且不超过5M) </div> </div> <!-- 图片和地图部分 --> <div class="media-section"> <el-row :gutter="20"> <el-col :span="12"> <div class="media-box"> <div class="media-title">事件图片</div> <div class="media-content"> <el-image v-if="currentDetail.mediaUrl" :src="getThumbUrl(currentDetail.mediaUrl)" :preview-src-list="[getPreviewUrl(currentDetail.mediaUrl)]" fit="contain" style=" cursor: pointer" > <template #placeholder> <div class="image-placeholder"> <i class="el-icon-picture-outline"></i> <span>加载中...</span> </div> </template> <template #error> <div class="image-error"> <i class="el-icon-picture-outline"></i> <span>加载失败</span> </div> </template> </el-image> <div v-else class="no-media">暂无图片/视频</div> </div> </div> </el-col> <el-col :span="12"> <div class="media-box"> <!-- 根据状态显示不同的标题和内容 --> <template v-if="currentDetail.status === 4"> <div class="media-title">工单处理图片</div> <div class="media-content"> <el-image v-if="currentDetail.updatePhotoUrl" :src="getThumbUrl(currentDetail.updatePhotoUrl)" :preview-src-list="[getPreviewUrl(currentDetail.updatePhotoUrl)]" fit="fill" > <template #placeholder> <div class="image-placeholder"> <i class="el-icon-picture-outline"></i> <span>加载中...</span> </div> </template> <template #error> <div class="image-error"> <i class="el-icon-picture-outline"></i> <span>加载失败</span> </div> </template> </el-image> <div v-else class="no-media">暂无处理图片</div> </div> </template> <template v-else> <div class="media-title"> 地图标记事件点 <el-popover v-if="currentDetail.status === 3 || currentDetail.status === 0" popper-class="custom-qrcode-popover" :width="120" :visible="currentDetail.showQR && detailVisible" placement="top" title="" trigger="click" > <template #reference> <img @click.stop="handleQRCode(currentDetail)" class="QRCodeImg" src="@/assets/images/dataCenter/qrCode.svg" alt="" title="事件导航" /> </template> <div class="qrcode-content"> <div class="close-btn" @click.stop="currentDetail.showQR = false">×</div> <CreateQRcode v-if="currentDetail.showQR && detailVisible" :latAndLon="currentDetail.location" ></CreateQRcode> </div> </el-popover> </div> <div class="media-content"> <map-container v-if="detailVisible" ref="MapContainer"></map-container> </div> </template> </div> </el-col> </el-row> </div> </div> <!-- 操作按钮 --> <div class="dialog-footer1-new"> <div class="leftBtn" :class="currentIndex === 0 ? 'disableds' : ''" @click="leftClick"> 上一页 </div> <div class="btngroups"> <template v-if="currentDetail.status === 2"> <!-- 待审核 --> <el-button v-if="hasReviewBtnPermission()" type="primary" :loading="approveLoading" @click="approveTicket" icon="el-icon-check" >通过 </el-button> <el-button v-if="hasReviewBtnPermission()" type="danger" :loading="rejectLoading" @click="rejectTicket" icon="el-icon-close" >不通过 </el-button> <el-button @click="detailVisible = false" icon="el-icon-circle-close">取消</el-button> </template> <template v-else-if="currentDetail.status === 0"> <el-button v-if="hasProcessingBtnPermission()" type="primary" :loading="dispatchLoading" @click="approveAndDispatch" icon="el-icon-check" >受理 </el-button> <el-button v-if="hasProcessingBtnPermission()" type="danger" :loading="rejectLoading" @click="rejectTicket" icon="el-icon-close" >不受理 </el-button> <el-button @click="detailVisible = false" icon="el-icon-circle-close">取消</el-button> </template> <template v-if="currentDetail.status === 3"> <!-- 处理中 --> <el-button v-if="hasProcessedAndOverBtnPermission()" type="primary" :loading="completeLoading" @click="completeTicket" icon="el-icon-circle-check" >完成工单 </el-button> <el-button @click="detailVisible = false" icon="el-icon-circle-close">取消</el-button> </template> <template v-else-if="currentDetail.status === 4"> <!-- 已完成 --> <!-- <el-button v-if="hasProcessedAndOverBtnPermission()" type="primary" :loading="finalizeLoading" @click="finalizeTicket">完结工单</el-button> --> <el-button @click="detailVisible = false" icon="el-icon-circle-close">取消</el-button> </template> </div> <div :class="currentIndex === tableData.length - 1 ? 'disableds' : ''" class="leftBtn" @click="rightClick" > 下一页 </div> </div> </div> </el-dialog> <ticket-detail-dialog v-model="detailVisible" :current-detail="currentDetail" :current-index="currentIndex" :total-items="tableData.length" :algorithms="algorithms" :types="types" :permission="permission" :step-status-list="stepStatusList" :work-type="workType" @detail-success="handleDetailSuccess" @detail-error="handleDetailError" @prev-item="handlePrevItem" @next-item="handleNextItem" @update:current-detail="handleCurrentDetailUpdate" @approve-and-dispatch="handleApproveAndDispatch" /> <!-- 派发工单对话框 --> <dispatch-dialog @@ -798,6 +434,7 @@ import RecheckDialog from '@/views/tickets/ticketComponent/RecheckDialog.vue'; import DispatchDialog from '@/views/tickets/ticketComponent/DispatchDialog.vue'; import CreateTicketDialog from '@/views/tickets/ticketComponent/CreateTicketDialog.vue'; import TicketDetailDialog from '@/views/tickets/ticketComponent/TicketDetailDialog.vue'; const { envName } = getBaseConfig(); function regExp(label, name) { @@ -806,18 +443,12 @@ } export default { components: { elTooltipCopy, CreateQRcode,RecheckDialog,DispatchDialog,CreateTicketDialog, }, components: { elTooltipCopy, CreateQRcode,RecheckDialog,DispatchDialog,CreateTicketDialog,TicketDetailDialog, }, name: 'TicketPage', data() { return { currentIndex: null, // 当前显示的数据索引 approveLoading: false, rejectLoading: false, dispatchLoading: false, completeLoading: false, finalizeLoading: false, createoredit: '', activeTab: 'all', // tabs 只保留静态结构,不做权限判断 @@ -937,8 +568,6 @@ dispatchDepartment: '', // 新增:派发部门 dispatchHandler: '', // 新增:派发处理人 dispatchDialogVisible: false, // 新增:派发对话框可见性 stepInfos: [], // 新增:存储步骤信息 fixedStatuses: ['2', '0', '3', '4'], // 固定的五个状态 userNameToIdMap: {}, // 新增用户名到ID的映射 workType: 0, // 新增:当前工单work_type @@ -946,7 +575,7 @@ reviewDialogVisible: false, // 新增:审核对话框可见性 currentReviewImage: '', // 新增:当前审核图片 currentImageIndex: 1, // 新增:当前图片索引 totalTime: '', isShowInfo: false, // 配置时间选择器默认配置 @@ -1310,32 +939,24 @@ }); } }, // 左切换 leftClick() { if (this.tableData.length === 0) return; this.currentIndex = Math.max(0, this.currentIndex - 1); this.updateCurrentDetail(); }, // 右切换 rightClick() { if (this.tableData.length === 0) return; this.currentIndex = Math.min(this.tableData.length - 1, this.currentIndex + 1); this.updateCurrentDetail(); }, // 更新当前详情 updateCurrentDetail() { updateCurrentDetail() { this.currentDetail = this.tableData[this.currentIndex]; this.currentDetail.mediaUrl = this.currentDetail.photo_url; this.currentDetail.updatePhotoUrl = this.currentDetail.update_photo_url; this.currentDetail.processingDetail = this.currentDetail.content; this.handleViewDetail(this.currentDetail); // 如果使用地图组件,需要更新地图标记 this.$nextTick(() => { if (this.$refs.MapContainer && this.currentDetail.location) { this.$refs.MapContainer.initAddEntity('point', this.currentDetail.location); } }); this.currentDetail.showQR = false; this.currentDetail.latAndLon = _.round(this.currentDetail.location[0], 6) + ',' + _.round(this.currentDetail.location[1], 6); }, // 更新当前详情数据 handleCurrentDetailUpdate(updatedDetail) { this.currentDetail = updatedDetail; }, // 受理并派发 handleApproveAndDispatch(detail) { this.currentDetail = detail; this.dispatchDialogVisible = true; }, async handleQRCode(val) { val.showQR = !val.showQR; @@ -1713,89 +1334,46 @@ } return `${location[0].toFixed(6)}, ${location[1].toFixed(6)}`; }, async handleViewDetail(row) { // 找到当前行在tableData中的索引 handleViewDetail(row) { this.currentIndex = this.tableData.findIndex(item => item.id === row.id); // 先设置workType,直接从row读取 this.workType = row.work_type !== undefined ? Number(row.work_type) : 0; // 重置上传组件的文件列表 this.$nextTick(() => { if (this.$refs.MapContainer && this.$refs.MapContainer.initAddEntity) { this.$refs.MapContainer.initAddEntity('point', this.currentDetail.location); } }); const detailData = { ...row, processingDetail: row.processing_details || '', mediaUrl: row.photo_url || row.video_url || '', updatePhotoUrl: row.update_photo_url || '', photos: [], job_name: row.job_name || '', // 新增 }; let stepArr = []; try { const stepResponse = await getStepInfo(row.orderNumber); const steps = Array.isArray(stepResponse.data.data) ? stepResponse.data.data : stepResponse.data.data?.steps || []; const finishedStep = steps.find(s => String(s.status) === '4'); this.totalTime = finishedStep && finishedStep.total_time ? finishedStep.total_time : ''; if (this.activeTab !== 'myTickets') { this.stepInfos = steps.map(step => ({ status: String(step.status), name: step.name, time: step.time, create_time: step.create_time, })); } else { const statusArr = this.workType === 1 ? ['3', '4'] : this.fixedStatuses; this.stepInfos = statusArr.map(status => { const step = steps.find(s => String(s.status) === String(status)); return { status, name: step ? step.name : '', time: step ? step.time : null, create_time: step ? step.create_time : null, }; }); } this.currentDetail.status = row.status; } catch (error) { if (this.activeTab === 'myTickets') { const statusArr = this.workType === 1 ? ['3', '4'] : this.fixedStatuses; this.stepInfos = statusArr.map(status => ({ status, name: status === row.status ? row.handler || '未分配' : '未处理', time: status === row.status ? row.startTime || '未知时间' : null, })); } else { this.stepInfos = []; } } console.log(detailData, 'detailDatadetailDatadetailData'); this.currentDetail = { ...detailData, // address: null, job_name: row.job_name || '', showQR: false, latAndLon: _.round(detailData.location[0], 6) + ',' + _.round(detailData.location[1], 6), latAndLon: _.round(row.location[0], 6) + ',' + _.round(row.location[1], 6), }; console.log('this.currentDetail', this.currentDetail); this.currentDetail = detailData; this.detailVisible = true; this.$nextTick(() => { if (this.$refs.MapContainer && this.$refs.MapContainer.initAddEntity) { this.$refs.MapContainer.initAddEntity('point', this.currentDetail.location); } }); }, // 详情成功回调 handleDetailSuccess() { this.fetchTableData(); }, // 详情错误回调 handleDetailError(error) { console.error('工单操作失败:', error); }, // 上一项 handlePrevItem() { this.currentIndex = Math.max(0, this.currentIndex - 1); this.updateCurrentDetail(); }, // 下一项 handleNextItem() { this.currentIndex = Math.min(this.tableData.length - 1, this.currentIndex + 1); this.updateCurrentDetail(); }, getStepHandler(status) { const step = this.stepInfos.find(step => step.status === status); @@ -1908,57 +1486,8 @@ 工单状态: this.mapStatus(Number(item.status || 0)), }; }, async approveTicket() { if (this.approveLoading) return; this.approveLoading = true; try { const data = { id: this.currentDetail.id, status: this.currentDetail.status, isPass: 0, // 0 表示通过 eventNum: this.currentDetail.orderNumber, }; const file = this.currentDetail.file || null; // 如果没有文件,则为 null const response = await flowEvent(data, file); if (response.data.code === 0) { this.$message.success('工单已通过'); this.detailVisible = false; this.fetchTableData(); } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { this.$message.error(error.message || '操作失败,请稍后重试'); } finally { this.approveLoading = false; } }, async rejectTicket() { if (this.rejectLoading) return; this.rejectLoading = true; try { const data = { id: this.currentDetail.id, status: this.currentDetail.status, isPass: 1, // 1 表示不通过 }; const response = await flowEvent(data); if (response.data.code === 0) { this.$message.success('工单未通过'); this.detailVisible = false; this.fetchTableData(); } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { this.$message.error(error.message || '操作失败,请稍后重试'); } finally { this.rejectLoading = false; } }, async submitProcessing() { if (this.currentDetail.status !== 3) { this.$message.warning('只有处理中状态的工单可以提交处理详情'); @@ -1991,47 +1520,7 @@ markAsCompleted() { this.$message.success('工单已标记为完成'); }, async completeTicket() { if (this.completeLoading) return; this.completeLoading = true; try { if (!this.currentDetail.processingDetail) { this.$message.warning('请先填写事件处理详情'); return; } // 检查图片上传 if (!this.currentDetail.photos || this.currentDetail.photos.length === 0) { this.$message.warning('请选择上传图片'); this.completeLoading = false; return; } const data = { id: this.currentDetail.id, status: this.currentDetail.status, processingDetails: this.currentDetail.processingDetail, eventNum: this.currentDetail.orderNumber, }; // 如果有上传的图片,添加到请求中 const file = this.currentDetail.photos?.[0]?.raw || null; const response = await flowEvent(data, file); if (response.data.code === 0) { this.$message.success('工单已完成'); this.detailVisible = false; this.fetchTableData(); // 刷新列表数据 } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { console.error('完成工单失败:', error); this.$message.error(error.message || '操作失败,请稍后重试'); } finally { this.completeLoading = false; } }, async approveAndDispatch() { if (this.dispatchLoading) return; @@ -2568,10 +2057,7 @@ margin-bottom: 32px; } .tableCss { width: 100%; margin-bottom: 10px; } .step-timer { position: absolute; @@ -2584,12 +2070,7 @@ font-size: 12px; } .event-total-time { text-align: center; color: #666; font-size: 15px; margin-bottom: 12px; } .action-bar { margin-bottom: 16px; @@ -2763,65 +2244,65 @@ } } .status-flow { margin-bottom: 20px; // .status-flow { // margin-bottom: 20px; .custom-steps { .el-step__description { position: relative; margin: 0; padding: 0; } // .custom-steps { // .el-step__description { // position: relative; // margin: 0; // padding: 0; // } // 添加发起任务步骤的特殊样式 .init-step-info { display: flex; flex-direction: column; align-items: center; text-align: center; width: 130px; // // 添加发起任务步骤的特殊样式 // .init-step-info { // display: flex; // flex-direction: column; // align-items: center; // text-align: center; // width: 130px; .creator-name { font-size: 14px; font-weight: bold; color: #303133; margin-bottom: 4px; } // .creator-name { // font-size: 14px; // font-weight: bold; // color: #303133; // margin-bottom: 4px; // } .create-time { font-size: 10px; color: #909399; } } // .create-time { // font-size: 10px; // color: #909399; // } // } // 保持其他步骤的原有样式 .step-info { display: flex; margin: 0; padding: 0; justify-content: space-between; align-items: center; width: 200px; // // 保持其他步骤的原有样式 // .step-info { // display: flex; // margin: 0; // padding: 0; // justify-content: space-between; // align-items: center; // width: 200px; .process-time { font-size: 10px; color: #909399; text-align: left; margin-left: -50%; padding-left: 0%; flex: 1; } // .process-time { // font-size: 10px; // color: #909399; // text-align: left; // margin-left: -50%; // padding-left: 0%; // flex: 1; // } .handler-name { font-size: 14px; font-weight: bold; color: #303133; text-align: left; flex: 1; } } } } // .handler-name { // font-size: 14px; // font-weight: bold; // color: #303133; // text-align: left; // flex: 1; // } // } // } // } .basic-info { margin-bottom: 20px; @@ -2855,11 +2336,7 @@ opacity: 0.3 !important; } .PopUpTableScrolls { max-height: 600px; overflow-y: scroll; overflow-x: hidden; } .media-box { width: 100%; @@ -2970,39 +2447,9 @@ color: #f78989; } .custom-steps-container { width: 100%; margin: 10px 0; } .steps-titles { display: flex; justify-content: space-between; margin-bottom: 14px; position: relative; } .step-title { text-align: center; flex: 1; font-size: 14px; color: #999; position: relative; padding-bottom: 5px; } .step-title.active { color: #409eff; font-weight: bold; } .event-title-center { text-align: center; font-size: 20px; font-weight: bold; margin-bottom: 5px; color: #333; } .custom-steps { margin-top: -20px; @@ -3013,18 +2460,7 @@ } } .step-description { font-size: 14px; color: #666; line-height: 1.5; display: block; text-align: center; &:first-child { font-weight: bold; margin-bottom: 4px; } } // 覆盖其他相关样式 .status-flow { src/views/tickets/ticketComponent/TicketDetailDialog.vue
New file @@ -0,0 +1,1064 @@ <template> <el-dialog class="custom-dialog" align-center v-model="dialogVisible" :title="isEditMode ? '编辑工单' : '工单详情'" width="80%" append-to-body @close="handleClose" > <div class="detail-container"> <div class="detail-top-title"> <div class="event-title-center event-orderNumber"> {{ currentDetail.orderNumber || '工单编号' }} </div> <div class="event-title-center">{{ currentDetail.orderName || '事件名称' }}</div> </div> <div v-if="totalTime" class="event-total-time">总耗时:{{ totalTime }}</div> <!-- 工单状态流程 --> <div class="custom-steps-container"> <!-- 标题行 --> <div class="steps-titles"> <div v-for="(status, index) in stepStatusList" :key="index" :class="{ 'step-title': true, active: index <= stepStatusList.indexOf(String(currentDetail.status)), }" > {{ mapStatus(status) }} </div> </div> <!-- Element Steps 组件 --> <el-steps :active="getActiveStep() - 1" align-center class="custom-steps"> <el-step v-for="(status, index) in stepStatusList" :key="index"> <template #description> <span class="step-description"> {{ getStepHandler(status) }} </span> <div class="step-description" v-if="getStepTime(status)"> <span class="step-timer"> 耗时:{{ getStepTime(status) }} </span> </div> <div class="step-description"> {{ getStepCreateTime(status) }} </div> </template> </el-step> </el-steps> </div> <div class="PopUpTableScrolls"> <!-- 基本信息表格 --> <el-table :show-header="false" :data="formattedDetailFields" border class="tableCss"> <el-table-column prop="label1" label="基本信息" width="150"> <template #default="{ row }"> <span v-if=" currentDetail.status === 0 && (row.label1 === '关联算法' || row.label1 === '工单名称') " class="required-label" > <span class="required-star">*</span>{{ row.label1 }} </span> <span v-else>{{ row.label1 }}</span> </template> </el-table-column> <el-table-column> <template #default="{ row }"> <template v-if=" currentDetail.status === 0 && row.label1 === '工单名称' && hasProcessingBtnPermission() " > <el-input v-model="currentDetail.orderName" placeholder="请输入工单名称" class="required-input" /> </template> <template v-else-if=" currentDetail.status === 0 && row.label1 === '关联算法' && hasProcessingBtnPermission() " > <el-select v-model="currentDetail.aiType" placeholder="请选择关联算法" class="required-input" > <el-option v-for="item in algorithms" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template> <template v-else>{{ row.value1 }}</template> </template> </el-table-column> <el-table-column prop="label2" label="基本信息" width="150"> <template #default="{ row }"> <span v-if="currentDetail.status === 0 && row.label2 === '工单内容'" class="required-label" > <span class="required-star">*</span>{{ row.label2 }} </span> <span v-else>{{ row.label2 }}</span> </template> </el-table-column> <el-table-column> <template #default="{ row }"> <template v-if=" currentDetail.status === 0 && row.label2 === '工单内容' && hasProcessingBtnPermission() " > <el-input type="textarea" v-model="currentDetail.content" placeholder="请输入工单内容" class="required-input" /> </template> <template v-else>{{ row.value2 }}</template> </template> </el-table-column> </el-table> <!-- 事件处理详情 --> <div v-if="[3, 4].includes(currentDetail.status)" class="form-section"> <div class="section-title"> <template v-if="currentDetail.status === 3"> <span class="required-label"> <span class="required-star">*</span>事件处理详情 </span> </template> <template v-else> 事件处理详情</template> </div> <!-- 处理中状态显示输入框 --> <template v-if="currentDetail.status === 3 && hasProcessedAndOverBtnPermission()"> <el-input type="textarea" v-model="currentDetail.processingDetail" placeholder="请输入事件处理详情" :rows="4" style="width: 100%; margin-bottom: 10px" /> </template> <!-- 已完成和已完结状态显示只读文本 --> <template v-else> <div class="readonly-processing-detail"> {{ currentDetail.processingDetail }} </div> </template> </div> <!-- 上传图片 --> <div v-if="[3].includes(currentDetail.status)" class="form-section uploadImg"> <div class="section-title" v-if="hasProcessedAndOverBtnPermission()"> <template v-if="currentDetail.status === 3"> <span class="required-label"> <span class="required-star">*</span>上传图片 </span> </template> <template v-else> 上传图片</template> </div> <el-upload v-if="hasProcessedAndOverBtnPermission()" ref="uploadRef" :action="'#'" :auto-upload="false" list-type="picture-card" :on-change="handleFileChange" :on-remove="handleUploadRemove" :before-upload="beforeUpload" :file-list="currentDetail.photos || []" :limit="1" accept="image/*" class="detail-upload" > <template v-if="!currentDetail.photos || currentDetail.photos.length < 1"> <div class="el-icon-plus"> <span>+</span> </div> </template> </el-upload> <div class="el-upload__tip" v-if="hasProcessedAndOverBtnPermission()"> (上传照片即可完结工单,只能上传jpg、jpeg、png照片,且不超过5M) </div> </div> <!-- 图片和地图部分 --> <div class="media-section"> <el-row :gutter="20"> <el-col :span="12"> <div class="media-box"> <div class="media-title">事件图片</div> <div class="media-content"> <el-image v-if="currentDetail.mediaUrl" :src="getThumbUrl(currentDetail.mediaUrl)" :preview-src-list="[getPreviewUrl(currentDetail.mediaUrl)]" fit="contain" style="cursor: pointer" > <template #placeholder> <div class="image-placeholder"> <i class="el-icon-picture-outline"></i> <span>加载中...</span> </div> </template> <template #error> <div class="image-error"> <i class="el-icon-picture-outline"></i> <span>加载失败</span> </div> </template> </el-image> <div v-else class="no-media">暂无图片/视频</div> </div> </div> </el-col> <el-col :span="12"> <div class="media-box"> <template v-if="currentDetail.status === 4"> <div class="media-title">工单处理图片</div> <div class="media-content"> <el-image v-if="currentDetail.updatePhotoUrl" :src="getThumbUrl(currentDetail.updatePhotoUrl)" :preview-src-list="[getPreviewUrl(currentDetail.updatePhotoUrl)]" fit="fill" > <template #placeholder> <div class="image-placeholder"> <i class="el-icon-picture-outline"></i> <span>加载中...</span> </div> </template> <template #error> <div class="image-error"> <i class="el-icon-picture-outline"></i> <span>加载失败</span> </div> </template> </el-image> <div v-else class="no-media">暂无处理图片</div> </div> </template> <template v-else> <div class="media-title"> 地图标记事件点 <el-popover v-if="currentDetail.status === 3 || currentDetail.status === 0" popper-class="custom-qrcode-popover" :width="120" :visible="currentDetail.showQR && dialogVisible" placement="top" title="" trigger="click" > <template #reference> <img @click.stop="handleQRCode(currentDetail)" class="QRCodeImg" src="@/assets/images/dataCenter/qrCode.svg" alt="" title="事件导航" /> </template> <div class="qrcode-content"> <div class="close-btn" @click.stop="currentDetail.showQR = false">×</div> <CreateQRcode v-if="currentDetail.showQR && dialogVisible" :lat-and-lon="currentDetail.location" /> </div> </el-popover> </div> <div class="media-content"> <map-container v-if="dialogVisible" ref="mapContainerRef" /> </div> </template> </div> </el-col> </el-row> </div> </div> <!-- 操作按钮 --> <div class="dialog-footer1-new"> <div :class="currentIndex === 0 ? 'disableds' : ''" class="leftBtn" @click="handlePrev" > 上一页 </div> <div class="btngroups"> <template v-if="currentDetail.status === 2"> <!-- 待审核 --> <el-button v-if="hasReviewBtnPermission()" type="primary" :loading="approveLoading" @click="handleApprove" icon="el-icon-check" > 通过 </el-button> <el-button v-if="hasReviewBtnPermission()" type="danger" :loading="rejectLoading" @click="handleReject" icon="el-icon-close" > 不通过 </el-button> <el-button @click="handleClose" icon="el-icon-circle-close">取消</el-button> </template> <template v-else-if="currentDetail.status === 0"> <el-button v-if="hasProcessingBtnPermission()" type="primary" :loading="dispatchLoading" @click="handleApproveAndDispatch" icon="el-icon-check" > 受理 </el-button> <el-button v-if="hasProcessingBtnPermission()" type="danger" :loading="rejectLoading" @click="handleReject" icon="el-icon-close" > 不受理 </el-button> <el-button @click="handleClose" icon="el-icon-circle-close">取消</el-button> </template> <template v-if="currentDetail.status === 3"> <!-- 处理中 --> <el-button v-if="hasProcessedAndOverBtnPermission()" type="primary" :loading="completeLoading" @click="handleComplete" icon="el-icon-circle-check" > 完成工单 </el-button> <el-button @click="handleClose" icon="el-icon-circle-close">取消</el-button> </template> <template v-else-if="currentDetail.status === 4"> <!-- 已完成 --> <el-button @click="handleClose" icon="el-icon-circle-close">取消</el-button> </template> </div> <div :class="currentIndex === totalItems - 1 ? 'disableds' : ''" class="leftBtn" @click="handleNext" > 下一页 </div> </div> </div> </el-dialog> </template> <script> import { flowEvent, getStepInfo } from '@/api/tickets/ticket'; import { getSmallImg, getShowImg } from '@/utils/util'; import CreateQRcode from '@/components/CreateQRcode/CreateQRcode.vue'; export default { name: 'TicketDetailDialog', components: { CreateQRcode }, props: { modelValue: { type: Boolean, default: false }, currentDetail: { type: Object, default: () => ({}) }, currentIndex: { type: Number, default: 0 }, totalItems: { type: Number, default: 0 }, algorithms: { type: Array, default: () => [] }, types: { type: Array, default: () => [] }, permission: { type: Object, default: () => ({}) }, stepStatusList: { type: Array, default: () => [] }, workType: { type: Number, default: 0 } }, emits: [ 'update:modelValue', 'detail-success', 'detail-error', 'prev-item', 'next-item', 'update:currentDetail' ], data() { return { stepInfos: [], totalTime: '', approveLoading: false, rejectLoading: false, dispatchLoading: false, completeLoading: false, finalizeLoading: false }; }, computed: { dialogVisible: { get() { return this.modelValue; }, set(value) { this.$emit('update:modelValue', value); } }, isEditMode() { return this.currentDetail.status === -1; }, formattedDetailFields() { const fields = [ { label: '工单名称', value: this.currentDetail.orderName }, { label: '工单类型', value: this.types.find(t => t.value === this.currentDetail.type)?.label || this.currentDetail.type, }, { label: '关联任务', value: this.currentDetail.job_name || '/' }, { label: '工单创建人', value: this.currentDetail.creator }, { label: '当前状态', value: this.mapStatus(this.currentDetail.status) }, { label: '事件地址', value: this.currentDetail.address || this.currentDetail.latAndLon }, { label: '关联算法', value: this.algorithms.find(t => t.value === this.currentDetail.aiType)?.label || this.currentDetail.aiType, }, { label: '发起单位', value: this.currentDetail.department }, { label: '发起任务时间', value: this.currentDetail.startTime }, { label: '工单内容', value: this.currentDetail.content }, ]; const filteredFields = fields.filter(field => field.value !== '/'); const formattedFields = []; for (let i = 0; i < filteredFields.length; i += 2) { formattedFields.push({ label1: filteredFields[i]?.label || '', value1: filteredFields[i]?.value || (filteredFields[i]?.label ? '暂无数据' : ''), label2: filteredFields[i + 1]?.label || '', value2: filteredFields[i + 1]?.value || (filteredFields[i + 1]?.label ? '暂无数据' : ''), }); } return formattedFields; } }, watch: { modelValue(newVal) { if (newVal) { this.loadStepInfo(); this.initMap(); } }, currentDetail: { handler(newVal) { if (newVal && this.dialogVisible) { this.loadStepInfo(); this.initMap(); } }, deep: true } }, methods: { handleClose() { this.dialogVisible = false; }, handlePrev() { if (this.currentIndex > 0) { this.$emit('prev-item'); } }, handleNext() { if (this.currentIndex < this.totalItems - 1) { this.$emit('next-item'); } }, async loadStepInfo() { try { const stepResponse = await getStepInfo(this.currentDetail.orderNumber); const steps = Array.isArray(stepResponse.data.data) ? stepResponse.data.data : stepResponse.data.data?.steps || []; const finishedStep = steps.find(s => String(s.status) === '4'); this.totalTime = finishedStep && finishedStep.total_time ? finishedStep.total_time : ''; this.stepInfos = steps.map(step => ({ status: String(step.status), name: step.name, time: step.time, create_time: step.create_time, })); } catch (error) { console.error('加载步骤信息失败:', error); this.stepInfos = []; } }, initMap() { this.$nextTick(() => { if (this.$refs.mapContainerRef && this.$refs.mapContainerRef.initAddEntity && this.currentDetail.location) { this.$refs.mapContainerRef.initAddEntity('point', this.currentDetail.location); } }); }, mapStatus(status) { const statusTextMap = { '-1': '草稿', 2: '待审核', 0: '待处理', 3: '处理中', 4: '已完成', }; return statusTextMap[status] || '未知状态'; }, getStepHandler(status) { const step = this.stepInfos.find(step => step.status === status); return step ? step.name : ''; }, getStepTime(status) { const step = this.stepInfos.find(step => step.status === status); return step && step.time && step.time !== '0' ? step.time : false; }, getStepCreateTime(status) { const step = this.stepInfos.find(step => step.status === status); return step ? step.create_time : null; }, getActiveStep() { const arr = this.stepStatusList; const index = arr.indexOf(String(this.currentDetail.status)); return index !== -1 ? index + 2 : 1; }, handleQRCode(val) { this.$emit('update:currentDetail', { ...this.currentDetail, showQR: !val.showQR }); }, getThumbUrl(url) { if (!url) return ''; return getSmallImg(url); }, getPreviewUrl(url) { if (!url) return ''; return getShowImg(url); }, hasProcessingBtnPermission() { return this.permission && this.permission.tickets_processing_btn === true; }, hasProcessedAndOverBtnPermission() { return this.permission && this.permission.tickets_view_processedAndOver === true; }, hasReviewBtnPermission() { return this.permission && this.permission.tickets_review_btn === true; }, // 文件操作方法 handleFileChange(file, fileList) { this.$emit('update:currentDetail', { ...this.currentDetail, photos: fileList }); }, handleUploadRemove(file, fileList) { this.$emit('update:currentDetail', { ...this.currentDetail, photos: fileList }); }, beforeUpload(file) { const isImage = file.type.includes('image'); const isLt5M = file.size / 1024 / 1024 < 5; if (!isImage) { this.$message.error('只能上传图片文件!'); return false; } if (!isLt5M) { this.$message.error('图片大小不能超过5MB!'); return false; } return true; }, // 工单操作方法 async handleApprove() { if (this.approveLoading) return; this.approveLoading = true; try { const data = { id: this.currentDetail.id, status: this.currentDetail.status, isPass: 0, eventNum: this.currentDetail.orderNumber, }; const response = await flowEvent(data); if (response.data.code === 0) { this.$message.success('工单已通过'); this.$emit('detail-success'); this.handleClose(); } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { this.$message.error(error.message || '操作失败,请稍后重试'); this.$emit('detail-error', error); } finally { this.approveLoading = false; } }, async handleReject() { if (this.rejectLoading) return; this.rejectLoading = true; try { const data = { id: this.currentDetail.id, status: this.currentDetail.status, isPass: 1, }; const response = await flowEvent(data); if (response.data.code === 0) { this.$message.success('工单未通过'); this.$emit('detail-success'); this.handleClose(); } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { this.$message.error(error.message || '操作失败,请稍后重试'); this.$emit('detail-error', error); } finally { this.rejectLoading = false; } }, handleApproveAndDispatch() { // 触发派发工单操作,由父组件处理 this.$emit('approve-and-dispatch', this.currentDetail); }, async handleComplete() { if (this.completeLoading) return; this.completeLoading = true; try { if (!this.currentDetail.processingDetail) { this.$message.warning('请先填写事件处理详情'); return; } if (!this.currentDetail.photos || this.currentDetail.photos.length === 0) { this.$message.warning('请选择上传图片'); this.completeLoading = false; return; } const data = { id: this.currentDetail.id, status: this.currentDetail.status, processingDetails: this.currentDetail.processingDetail, eventNum: this.currentDetail.orderNumber, }; const file = this.currentDetail.photos?.[0]?.raw || null; const response = await flowEvent(data, file); if (response.data.code === 0) { this.$message.success('工单已完成'); this.$emit('detail-success'); this.handleClose(); } else { throw new Error(response.data.msg || '操作失败'); } } catch (error) { console.error('完成工单失败:', error); this.$message.error(error.message || '操作失败,请稍后重试'); this.$emit('detail-error', error); } finally { this.completeLoading = false; } } } }; </script> <style lang="scss" scoped> .custom-dialog { max-height: 96vh; :deep(.el-dialog__body) { border-top: 0.1rem solid #f0f0f0; } } .detail-container { padding: 0 20px; .detail-top-title { display: flex; justify-content: center; align-items: center; .event-orderNumber { margin-right: 10px; } } } .event-title-center { text-align: center; font-size: 20px; font-weight: bold; margin-bottom: 5px; color: #333; } .event-total-time { text-align: center; color: #666; font-size: 15px; margin-bottom: 12px; } .custom-steps-container { width: 100%; margin: 10px 0; } .steps-titles { display: flex; justify-content: space-between; margin-bottom: 14px; position: relative; } .step-title { text-align: center; flex: 1; font-size: 14px; color: #999; position: relative; padding-bottom: 5px; } .step-title.active { color: #409eff; font-weight: bold; } .status-flow { margin-bottom: 20px; .custom-steps { .el-step__description { position: relative; margin: 0; padding: 0; } // 添加发起任务步骤的特殊样式 .init-step-info { display: flex; flex-direction: column; align-items: center; text-align: center; width: 130px; .creator-name { font-size: 14px; font-weight: bold; color: #303133; margin-bottom: 4px; } .create-time { font-size: 10px; color: #909399; } } // 保持其他步骤的原有样式 .step-info { display: flex; margin: 0; padding: 0; justify-content: space-between; align-items: center; width: 200px; .process-time { font-size: 10px; color: #909399; text-align: left; margin-left: -50%; padding-left: 0%; flex: 1; } .handler-name { font-size: 14px; font-weight: bold; color: #303133; text-align: left; flex: 1; } } } } .step-description { font-size: 14px; color: #666; line-height: 1.5; display: block; text-align: center; &:first-child { font-weight: bold; margin-bottom: 4px; } } .PopUpTableScrolls { max-height: 600px; overflow-y: scroll; overflow-x: hidden; } .tableCss { width: 100%; margin-bottom: 10px; } /* 必填项样式 */ .required-label { position: relative; display: inline-block; .required-star { color: #f56c6c; margin-right: 4px; } } /* 必填输入框样式 */ .required-input { width: 100%; :deep(.el-input__inner), :deep(.el-textarea__inner) { &:focus { border-color: #409eff; } } } /* 新建工单和处理工单的上传组件样式 */ .create-upload, .detail-upload { :deep(.el-upload--picture-card) { width: 120px; height: 100px; line-height: 100px; } /* 隐藏额外的上传按钮 */ :deep(.el-upload.el-upload--picture-card) { display: none; } /* 当没有图片时显示上传按钮 */ :deep(.el-upload.el-upload--picture-card:first-child) { display: flex; } /* 上传组件的预览图片样式 */ :deep(.el-upload-list--picture-card .el-upload-list__item) { width: 120px; height: 100px; } } .el-upload__tip { font-size: 12px; color: #909399; margin-top: 12px; } .media-section { // margin-bottom: 20px; .el-row { display: flex; align-items: center; } } .media-box { width: 100%; border: 1px solid #dcdfe6; border-radius: 4px; padding: 10px; background: #fff; box-sizing: border-box; .media-title { font-weight: bold; margin-bottom: 5px; display: flex; align-items: center; .QRCodeImg { width: 18px; height: 18px; cursor: pointer; padding-bottom: 1px; } } .media-content { position: relative; height: 500px; :deep(.el-image) { width: 100% !important; max-width: 100%; max-height: 100%; .el-image__inner { width: 100%; height: 100%; object-fit: cover !important; } } } } .image-placeholder, .image-error, .no-media { height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; background-color: #f5f7fa; border-radius: 4px; i { font-size: 32px; margin-bottom: 8px; } } .no-media { border: 1px dashed #d9d9d9; } .dialog-footer1-new { position: sticky; bottom: 28px; left: 0; right: 0; background: white; z-index: 10; padding: 16px 20px; border-top: 1px solid #ebeef5; display: flex; justify-content: space-between; align-items: center; .el-button + .el-button { margin-left: 12px; } .btngroups { margin-left: 12px; } .el-button { padding: 9px 20px; &:last-child { margin-left: 12px; margin-right: 12px; } } } .leftBtn { width: 70px; height: 32px; background-color: #999; border-radius: 5px; text-align: center; line-height: 32px; color: #fff; cursor: pointer; opacity: 0.8; } .disableds { background: #999 !important; cursor: not-allowed !important; pointer-events: none; opacity: 0.3 !important; } </style>