Selaa lähdekoodia

refactor(boiler&pipe checker): 优化整改附件展示与回退流程

1. 替换原生img/video标签为el-image组件,优化图片预览体验
2. 重构视频展示样式,添加播放覆盖层与点击预览逻辑
3. 新增面板收缩状态下的回退原因弹窗,完善回退操作流程
4. 提取通用的审批执行逻辑,优化代码复用性
xuzhancheng 5 päivää sitten
vanhempi
commit
7c64e1df8f

+ 101 - 59
yudao-ui-admin-vue3/src/views/pressure2/boilerchecker/components/StatusOperationPanel.vue

@@ -184,13 +184,16 @@
                     <div class="item-content1-img">
                       <template
                         v-for="(path, index) in checkBookDetail.rectificationImage?.split(',') || []"
-                        :key="index"
+                        :key="'img' + index"
                       >
                         <div class="item-content1-img-item">
-                          <img class="img-item" :src="buildFileUrl(path)" alt="" @click="handlePreview(buildFileUrl(path), 'image')" />
-                          <div class="list-item-tool">
-                            <el-icon class="icon"  @click="() => handlePreview(buildFileUrl(path), 'image')"><View /></el-icon>
-                          </div>
+                          <el-image
+                            class="img-item"
+                            :src="buildFileUrl(path)"
+                            :preview-src-list="[buildFileUrl(path)]"
+                            fit="cover"
+                            preview-teleported
+                          />
                         </div>
                       </template>
                     </div>
@@ -200,13 +203,12 @@
                     <div class="item-content1-img">
                       <template
                         v-for="(path, index) in checkBookDetail.rectificationVideo?.split(',') || []"
-                        :key="index"
+                        :key="'vid' + index"
                       >
-                        <div class="item-content1-img-item">
-                          <video class="img-item" :src="buildFileUrl(path)" alt="" @click="handlePreview(buildFileUrl(path), 'video')"></video>
-                          <!-- 视频播放按钮 -->
-                          <div class="list-item-tool">
-                            <el-icon class="icon"  @click="() => handlePreview(buildFileUrl(path), 'video')"><View /></el-icon>
+                        <div class="item-content1-img-item" @click="handlePreview(buildFileUrl(path), 'video')">
+                          <video class="img-item" :src="buildFileUrl(path)"></video>
+                          <div class="video-play-overlay">
+                            <el-icon :size="28"><VideoPlay /></el-icon>
                           </div>
                         </div>
                       </template>
@@ -603,6 +605,30 @@
       :reportList="reportList"
       @confirm="handleRefresh"
     />
+
+    <!-- 回退原因弹窗(面板收缩时使用) -->
+    <el-dialog
+      v-model="rejectionReasonDialogVisible"
+      title="请输入回退原因"
+      width="450px"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="rejectionReasonDialogFormRef" :model="tempRejectionReason" :rules="returnFormRules">
+        <el-form-item label="回退原因" prop="rejectionReason">
+          <el-input
+            v-model="tempRejectionReason.rejectionReason"
+            type="textarea"
+            :rows="5"
+            maxlength="100"
+            placeholder="请输入回退原因"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectionReasonDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleRejectionReasonDialogConfirm">确定回退</el-button>
+      </template>
+    </el-dialog>
   </div>
 
   <!-- 新加 -->
@@ -832,7 +858,7 @@ import SmartTable from '@/components/SmartTable/SmartTable'
 import CustomUploadFile from '@/components/CustomUploadFile/index.vue'
 import { computed, nextTick, reactive, ref, watch } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
-import { InfoFilled, View, Back, Right, WarningFilled } from '@element-plus/icons-vue'
+import { InfoFilled, View, Back, Right, WarningFilled, VideoPlay } from '@element-plus/icons-vue'
 import { dayjs, ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
 const CustomDialog = defineAsyncComponent(() => import('@/components/CustomDialog/index.vue'))
 import VuePdfEmbed from 'vue-pdf-embed'
@@ -1255,6 +1281,10 @@ const returnFormRules = ref({
 })
 const returnFormRef = ref()
 
+const rejectionReasonDialogVisible = ref(false)
+const tempRejectionReason = ref({ rejectionReason: '' })
+const rejectionReasonDialogFormRef = ref()
+
 const isCompleteInput = computed(() => {
   return props.selectedItem?.taskStatus >= PressureCheckerMyTaskStatus.RECORD_CHECK
 })
@@ -1270,10 +1300,44 @@ const handlePass = async (type) => {
     approvalType: type
   }
   if (type === 1) {
-    const valid = await returnFormRef.value.validate()
-    if (!valid) return
-    params.rejectionReason = returnForm.value.rejectionReason
+    if (isExpanded.value) {
+      try {
+        await returnFormRef.value.validate()
+      } catch {
+        ElMessage.warning('请填写回退原因')
+        return
+      }
+      params.rejectionReason = returnForm.value.rejectionReason
+      executePass(params, type)
+    } else {
+      tempRejectionReason.value.rejectionReason = returnForm.value.rejectionReason
+      rejectionReasonDialogVisible.value = true
+    }
+    return
+  }
+  executePass(params, type)
+}
+
+// 弹出框确认回退
+const handleRejectionReasonDialogConfirm = async () => {
+  try {
+    await rejectionReasonDialogFormRef.value.validate()
+  } catch {
+    return
+  }
+  rejectionReasonDialogVisible.value = false
+  // 同步到主表单以便保持数据一致
+  returnForm.value.rejectionReason = tempRejectionReason.value.rejectionReason
+  const params: Record<string, any> = {
+    reportId: props.selectedItem?.id,
+    approvalType: 1,
+    rejectionReason: tempRejectionReason.value.rejectionReason
   }
+  executePass(params, 1)
+}
+
+// 执行通过/退回操作
+const executePass = (params: Record<string, any>, type: number) => {
   const tipText = type === 1 ? '退回' : type === 2 ? '整改不通过' : '整改通过'
   ElMessageBox.confirm(`确定${tipText}吗?`, '提示', {
     confirmButtonText: '确定',
@@ -2617,39 +2681,28 @@ watch(() => props.isFullscreen, () => {
               width: 100px;
               height: 100px;
               position: relative;
+              border-radius: 8px;
+              overflow: hidden;
+              cursor: pointer;
 
-              .list-item-tool {
+              .video-play-overlay {
                 position: absolute;
                 top: 0;
                 left: 0;
                 width: 100%;
                 height: 100%;
-                z-index: 99;
                 border-radius: 8px;
-                transition: all 0.5s ease;
-                cursor: pointer;
+                transition: all 0.3s ease;
                 display: flex;
                 justify-content: center;
                 align-items: center;
-                gap: 5px;
+                background-color: rgba(0, 0, 0, 0.2);
+                color: #ffffffcc;
+                pointer-events: none;
+
                 &:hover {
-                  backdrop-filter: blur(2px);
-                  background-color: #00000076;
-                  z-index: 1;
-                  .icon {
-                    display: inline-block;
-                    opacity: 1;
-                    &:hover {
-                      color: #ffffff;
-                      transition: all 0.5s ease;
-                    }
-                  }
-                }
-                .icon {
-                  color: #ffffff99;
-                  opacity: 0;
-                  transition: all 0.5s ease;
-                  font-size: 16px;
+                  background-color: rgba(0, 0, 0, 0.45);
+                  color: #ffffff;
                 }
               }
             }
@@ -2856,39 +2909,28 @@ watch(() => props.isFullscreen, () => {
             width: 100px;
             height: 100px;
             position: relative;
+            border-radius: 8px;
+            overflow: hidden;
+            cursor: pointer;
 
-            .list-item-tool {
+            .video-play-overlay {
               position: absolute;
               top: 0;
               left: 0;
               width: 100%;
               height: 100%;
-              z-index: 99;
               border-radius: 8px;
-              transition: all 0.5s ease;
-              cursor: pointer;
+              transition: all 0.3s ease;
               display: flex;
               justify-content: center;
               align-items: center;
-              gap: 5px;
+              background-color: rgba(0, 0, 0, 0.2);
+              color: #ffffffcc;
+              pointer-events: none;
+
               &:hover {
-                backdrop-filter: blur(2px);
-                background-color: #00000076;
-                z-index: 1;
-                .icon {
-                  display: inline-block;
-                  opacity: 1;
-                  &:hover {
-                    color: #ffffff;
-                    transition: all 0.5s ease;
-                  }
-                }
-              }
-              .icon {
-                color: #ffffff99;
-                opacity: 0;
-                transition: all 0.5s ease;
-                font-size: 16px;
+                background-color: rgba(0, 0, 0, 0.45);
+                color: #ffffff;
               }
             }
           }

+ 101 - 59
yudao-ui-admin-vue3/src/views/pressure2/pipechecker/components/StatusOperationPanel.vue

@@ -184,13 +184,16 @@
                     <div class="item-content1-img">
                       <template
                         v-for="(path, index) in checkBookDetail.rectificationImage?.split(',') || []"
-                        :key="index"
+                        :key="'img' + index"
                       >
                         <div class="item-content1-img-item">
-                          <img class="img-item" :src="buildFileUrl(path)" alt="" @click="handlePreview(buildFileUrl(path), 'image')" />
-                          <div class="list-item-tool">
-                            <el-icon class="icon"  @click="() => handlePreview(buildFileUrl(path), 'image')"><View /></el-icon>
-                          </div>
+                          <el-image
+                            class="img-item"
+                            :src="buildFileUrl(path)"
+                            :preview-src-list="[buildFileUrl(path)]"
+                            fit="cover"
+                            preview-teleported
+                          />
                         </div>
                       </template>
                     </div>
@@ -200,13 +203,12 @@
                     <div class="item-content1-img">
                       <template
                         v-for="(path, index) in checkBookDetail.rectificationVideo?.split(',') || []"
-                        :key="index"
+                        :key="'vid' + index"
                       >
-                        <div class="item-content1-img-item">
-                          <video class="img-item" :src="buildFileUrl(path)" alt="" @click="handlePreview(buildFileUrl(path), 'video')"></video>
-                          <!-- 视频播放按钮 -->
-                          <div class="list-item-tool">
-                            <el-icon class="icon"  @click="() => handlePreview(buildFileUrl(path), 'video')"><View /></el-icon>
+                        <div class="item-content1-img-item" @click="handlePreview(buildFileUrl(path), 'video')">
+                          <video class="img-item" :src="buildFileUrl(path)"></video>
+                          <div class="video-play-overlay">
+                            <el-icon :size="28"><VideoPlay /></el-icon>
                           </div>
                         </div>
                       </template>
@@ -603,6 +605,30 @@
       :reportList="reportList"
       @confirm="handleRefresh"
     />
+
+    <!-- 回退原因弹窗(面板收缩时使用) -->
+    <el-dialog
+      v-model="rejectionReasonDialogVisible"
+      title="请输入回退原因"
+      width="450px"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="rejectionReasonDialogFormRef" :model="tempRejectionReason" :rules="returnFormRules">
+        <el-form-item label="回退原因" prop="rejectionReason">
+          <el-input
+            v-model="tempRejectionReason.rejectionReason"
+            type="textarea"
+            :rows="5"
+            maxlength="100"
+            placeholder="请输入回退原因"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectionReasonDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleRejectionReasonDialogConfirm">确定回退</el-button>
+      </template>
+    </el-dialog>
   </div>
 
   <!-- 新加 -->
@@ -835,7 +861,7 @@ import SmartTable from '@/components/SmartTable/SmartTable'
 import CustomUploadFile from '@/components/CustomUploadFile/index.vue'
 import {computed, defineAsyncComponent, nextTick, reactive, ref, watch} from 'vue'
 import {useRoute} from 'vue-router'
-import { InfoFilled, View, Back, Right, WarningFilled } from '@element-plus/icons-vue'
+import { InfoFilled, View, Back, Right, WarningFilled, VideoPlay } from '@element-plus/icons-vue'
 import {dayjs, ElMessage, ElMessageBox, FormInstance, FormRules} from 'element-plus'
 import * as UserApi from '@/api/system/user'
 import {
@@ -1227,6 +1253,10 @@ const returnFormRules = ref({
 })
 const returnFormRef = ref()
 
+const rejectionReasonDialogVisible = ref(false)
+const tempRejectionReason = ref({ rejectionReason: '' })
+const rejectionReasonDialogFormRef = ref()
+
 const isCompleteInput = computed(() => {
   return props.selectedItem?.taskStatus >= PressureCheckerMyTaskStatus.RECORD_CHECK
 })
@@ -1242,10 +1272,44 @@ const handlePass = async (type) => {
     approvalType: type
   }
   if (type === 1) {
-    const valid = await returnFormRef.value.validate()
-    if (!valid) return
-    params.rejectionReason = returnForm.value.rejectionReason
+    if (isExpanded.value) {
+      try {
+        await returnFormRef.value.validate()
+      } catch {
+        ElMessage.warning('请填写回退原因')
+        return
+      }
+      params.rejectionReason = returnForm.value.rejectionReason
+      executePass(params, type)
+    } else {
+      tempRejectionReason.value.rejectionReason = returnForm.value.rejectionReason
+      rejectionReasonDialogVisible.value = true
+    }
+    return
   }
+  executePass(params, type)
+}
+
+// 弹出框确认回退
+const handleRejectionReasonDialogConfirm = async () => {
+  try {
+    await rejectionReasonDialogFormRef.value.validate()
+  } catch {
+    return
+  }
+  rejectionReasonDialogVisible.value = false
+  // 同步到主表单以便保持数据一致
+  returnForm.value.rejectionReason = tempRejectionReason.value.rejectionReason
+  const params: Record<string, any> = {
+    reportId: props.selectedItem?.id,
+    approvalType: 1,
+    rejectionReason: tempRejectionReason.value.rejectionReason
+  }
+  executePass(params, 1)
+}
+
+// 执行通过/退回操作
+const executePass = (params: Record<string, any>, type: number) => {
   const tipText = type === 1 ? '退回' : type === 2 ? '整改不通过' : '整改通过'
   ElMessageBox.confirm(`确定${tipText}吗?`, '提示', {
     confirmButtonText: '确定',
@@ -2516,39 +2580,28 @@ watch(() => props.isFullscreen, () => {
                 width: 100px;
                 height: 100px;
                 position: relative;
+                border-radius: 8px;
+                overflow: hidden;
+                cursor: pointer;
 
-                .list-item-tool {
+                .video-play-overlay {
                   position: absolute;
                   top: 0;
                   left: 0;
                   width: 100%;
                   height: 100%;
-                  z-index: 99;
                   border-radius: 8px;
-                  transition: all 0.5s ease;
-                  cursor: pointer;
+                  transition: all 0.3s ease;
                   display: flex;
                   justify-content: center;
                   align-items: center;
-                  gap: 5px;
+                  background-color: rgba(0, 0, 0, 0.2);
+                  color: #ffffffcc;
+                  pointer-events: none;
+
                   &:hover {
-                    backdrop-filter: blur(2px);
-                    background-color: #00000076;
-                    z-index: 1;
-                    .icon {
-                      display: inline-block;
-                      opacity: 1;
-                      &:hover {
-                        color: #ffffff;
-                        transition: all 0.5s ease;
-                      }
-                    }
-                  }
-                  .icon {
-                    color: #ffffff99;
-                    opacity: 0;
-                    transition: all 0.5s ease;
-                    font-size: 16px;
+                    background-color: rgba(0, 0, 0, 0.45);
+                    color: #ffffff;
                   }
                 }
               }
@@ -2755,39 +2808,28 @@ watch(() => props.isFullscreen, () => {
             width: 100px;
             height: 100px;
             position: relative;
+            border-radius: 8px;
+            overflow: hidden;
+            cursor: pointer;
 
-            .list-item-tool {
+            .video-play-overlay {
               position: absolute;
               top: 0;
               left: 0;
               width: 100%;
               height: 100%;
-              z-index: 99;
               border-radius: 8px;
-              transition: all 0.5s ease;
-              cursor: pointer;
+              transition: all 0.3s ease;
               display: flex;
               justify-content: center;
               align-items: center;
-              gap: 5px;
+              background-color: rgba(0, 0, 0, 0.2);
+              color: #ffffffcc;
+              pointer-events: none;
+
               &:hover {
-                backdrop-filter: blur(2px);
-                background-color: #00000076;
-                z-index: 1;
-                .icon {
-                  display: inline-block;
-                  opacity: 1;
-                  &:hover {
-                    color: #ffffff;
-                    transition: all 0.5s ease;
-                  }
-                }
-              }
-              .icon {
-                color: #ffffff99;
-                opacity: 0;
-                transition: all 0.5s ease;
-                font-size: 16px;
+                background-color: rgba(0, 0, 0, 0.45);
+                color: #ffffff;
               }
             }
           }