Explorar o código

fix:工单bug修改

null hai 1 semana
pai
achega
cc39debfe7

+ 12 - 3
src/api/work/order.js

@@ -44,12 +44,12 @@ export function processWorkOrder(data) {
   })
 }
 
-/** 完成/提交验收 */
-export function completeWorkOrder(orderId, result) {
+/** 完成/提交验收,提交维修后图片 */
+export function completeWorkOrder(data) {
   return request({
     url: '/api/work-order/complete',
     method: 'put',
-    params: { orderId, result }
+    data
   })
 }
 
@@ -70,6 +70,15 @@ export function cancelWorkOrder(orderId) {
   })
 }
 
+/** 驳回工单(待派单/待接单 -> 已驳回) */
+export function rejectWorkOrder(data) {
+  return request({
+    url: '/api/work-order/reject',
+    method: 'put',
+    data
+  })
+}
+
 /** 验收通过/结案 */
 export function closeWorkOrder(data) {
   return request({

+ 57 - 87
src/components/ImageUpload/index.vue

@@ -19,7 +19,6 @@
     >
       <el-icon class="avatar-uploader-icon"><plus /></el-icon>
     </el-upload>
-    <!-- 上传提示 -->
     <div class="el-upload__tip" v-if="showTip">
       请上传
       <template v-if="fileSize">
@@ -31,16 +30,8 @@
       的文件
     </div>
 
-    <el-dialog
-      v-model="dialogVisible"
-      title="预览"
-      width="800px"
-      append-to-body
-    >
-      <img
-        :src="dialogImageUrl"
-        style="display: block; max-width: 100%; margin: 0 auto"
-      />
+    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
     </el-dialog>
   </div>
 </template>
@@ -50,64 +41,51 @@ import { getToken } from "@/utils/auth";
 
 const props = defineProps({
   modelValue: [String, Object, Array],
-  // 图片数量限制
-  limit: {
-    type: Number,
-    default: 5,
-  },
-  // 大小限制(MB)
-  fileSize: {
-    type: Number,
-    default: 5,
-  },
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileType: {
-    type: Array,
-    default: () => ["png", "jpg", "jpeg"],
-  },
-  // 是否显示提示
-  isShowTip: {
-    type: Boolean,
-    default: true
-  },
+  limit: { type: Number, default: 5 },
+  fileSize: { type: Number, default: 5 },
+  fileType: { type: Array, default: () => ["png", "jpg", "jpeg"] },
+  isShowTip: { type: Boolean, default: true },
+  uploadUrl: { type: String, default: "" }
 });
 
 const { proxy } = getCurrentInstance();
-const emit = defineEmits();
+const emit = defineEmits(["update:modelValue"]);
 const number = ref(0);
 const uploadList = ref([]);
 const dialogImageUrl = ref("");
 const dialogVisible = ref(false);
 const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
+const uploadImgUrl = ref(props.uploadUrl || baseUrl + "/common/upload");
 const headers = ref({ Authorization: "Bearer " + getToken() });
 const fileList = ref([]);
-const showTip = computed(
-  () => props.isShowTip && (props.fileType || props.fileSize)
-);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
 
 watch(() => props.modelValue, val => {
-  if (val) {
-    // 首先将值转为数组
-    const list = Array.isArray(val) ? val : props.modelValue.split(",");
-    // 然后将数组转为对象数组
+  if (val && (Array.isArray(val) || typeof val === "object" || String(val).trim())) {
+    const list = Array.isArray(val)
+      ? val
+      : typeof val === "string"
+        ? val.split(",")
+        : [val];
     fileList.value = list.map(item => {
-      if (typeof item === "string") {
-        if (item.indexOf(baseUrl) === -1) {
-          item = { name: baseUrl + item, url: baseUrl + item };
-        } else {
-          item = { name: item, url: item };
-        }
-      }
-      return item;
-    });
+      const source = typeof item === "string" ? item : (item.url || item.fileName || item.name);
+      const url = normalizeImageUrl(source);
+      return typeof item === "object"
+        ? { ...item, name: item.name || source || url, url, status: item.status || "success" }
+        : { name: source || url, url, status: "success" };
+    }).filter(item => item.url);
   } else {
     fileList.value = [];
-    return [];
   }
-},{ deep: true, immediate: true });
+}, { deep: true, immediate: true });
+
+function normalizeImageUrl(value) {
+  const url = String(value || "").trim();
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url;
+  if (baseUrl && (url === baseUrl || url.startsWith(baseUrl + "/"))) return url;
+  return url.startsWith("/") ? baseUrl + url : baseUrl + "/" + url;
+}
 
-// 上传前loading加载
 function handleBeforeUpload(file) {
   let isImg = false;
   if (props.fileType.length) {
@@ -115,40 +93,37 @@ function handleBeforeUpload(file) {
     if (file.name.lastIndexOf(".") > -1) {
       fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
     }
-    isImg = props.fileType.some(type => {
-      if (file.type.indexOf(type) > -1) return true;
-      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
-      return false;
-    });
+    isImg = props.fileType.some(type => file.type.indexOf(type) > -1 || fileExtension.indexOf(type) > -1);
   } else {
     isImg = file.type.indexOf("image") > -1;
   }
   if (!isImg) {
-    proxy.$modal.msgError(
-      `文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
-    );
+    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
     return false;
   }
-  if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
+  if (props.fileSize && file.size / 1024 / 1024 >= props.fileSize) {
+    proxy.$modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`);
+    return false;
   }
   proxy.$modal.loading("正在上传图片,请稍候...");
   number.value++;
 }
 
-// 文件个数超出
 function handleExceed() {
   proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
 }
 
-// 上传成功回调
 function handleUploadSuccess(res, file) {
   if (res.code === 200) {
-    uploadList.value.push({ name: res.fileName, url: res.fileName });
+    const storedUrl = uploadImgUrl.value.includes("/minio/")
+      ? (res.url || res.fileName)
+      : (res.fileName || res.url);
+    uploadList.value.push({
+      name: res.originalFilename || storedUrl,
+      url: normalizeImageUrl(storedUrl),
+      status: "success",
+      uid: file.uid
+    });
     uploadedSuccessfully();
   } else {
     number.value--;
@@ -159,9 +134,10 @@ function handleUploadSuccess(res, file) {
   }
 }
 
-// 删除图片
 function handleDelete(file) {
-  const findex = fileList.value.map(f => f.name).indexOf(file.name);
+  const findex = fileList.value.findIndex(f =>
+    (file.uid && f.uid === file.uid) || f.url === file.url || f.name === file.name
+  );
   if (findex > -1 && uploadList.value.length === number.value) {
     fileList.value.splice(findex, 1);
     emit("update:modelValue", listToString(fileList.value));
@@ -169,7 +145,6 @@ function handleDelete(file) {
   }
 }
 
-// 上传结束处理
 function uploadedSuccessfully() {
   if (number.value > 0 && uploadList.value.length === number.value) {
     fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
@@ -180,34 +155,29 @@ function uploadedSuccessfully() {
   }
 }
 
-// 上传失败
 function handleUploadError() {
   proxy.$modal.msgError("上传图片失败");
   proxy.$modal.closeLoading();
 }
 
-// 预览
 function handlePictureCardPreview(file) {
   dialogImageUrl.value = file.url;
   dialogVisible.value = true;
 }
 
-// 对象转成指定字符串分隔
-function listToString(list, separator) {
-  let strs = "";
-  separator = separator || ",";
-  for (let i in list) {
-    if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
-      strs += list[i].url.replace(baseUrl, "") + separator;
-    }
-  }
-  return strs != "" ? strs.substr(0, strs.length - 1) : "";
+function listToString(list, separator = ",") {
+  return list
+    .filter(item => item.url && !item.url.startsWith("blob:"))
+    .map(item => {
+      const url = item.url;
+      return baseUrl && url.startsWith(baseUrl) ? url.slice(baseUrl.length) : url;
+    })
+    .join(separator);
 }
 </script>
 
 <style scoped lang="scss">
-// .el-upload--picture-card 控制加号部分
 :deep(.hide .el-upload--picture-card) {
-    display: none;
+  display: none;
 }
-</style>
+</style>

+ 239 - 0
src/components/RepairImagePanel/index.vue

@@ -0,0 +1,239 @@
+<template>
+  <section class="repair-image-panel">
+    <div class="repair-image-panel__header">
+      <div class="repair-image-panel__title">
+        <el-icon><Camera /></el-icon>
+        <span>维修前后图片</span>
+      </div>
+      <span class="repair-image-panel__total">共 {{ allImages.length }} 张</span>
+    </div>
+
+    <div class="repair-image-panel__body">
+      <div class="repair-image-column">
+        <div class="repair-image-column__header">
+          <span>维修前</span>
+          <el-tag size="small" type="info">{{ beforeImageList.length }} 张</el-tag>
+        </div>
+        <div v-if="beforeImageList.length" class="repair-image-list">
+          <div
+            v-for="(img, index) in beforeImageList"
+            :key="`before-${index}-${img}`"
+            class="repair-image-item"
+          >
+            <el-image
+              class="repair-image-media"
+              :src="img"
+              :preview-src-list="beforeImageList"
+              :initial-index="index"
+              fit="cover"
+              preview-teleported
+            >
+              <template #error>
+                <div class="repair-image-error">图片加载失败</div>
+              </template>
+            </el-image>
+            <span class="repair-image-item__index">{{ index + 1 }}</span>
+          </div>
+        </div>
+        <div v-else class="repair-image-empty">暂无维修前图片</div>
+      </div>
+
+      <div class="repair-image-column">
+        <div class="repair-image-column__header">
+          <span>维修后</span>
+          <el-tag size="small" type="success">{{ afterImageList.length }} 张</el-tag>
+        </div>
+        <div v-if="afterImageList.length" class="repair-image-list">
+          <div
+            v-for="(img, index) in afterImageList"
+            :key="`after-${index}-${img}`"
+            class="repair-image-item"
+          >
+            <el-image
+              class="repair-image-media"
+              :src="img"
+              :preview-src-list="afterImageList"
+              :initial-index="index"
+              fit="cover"
+              preview-teleported
+            >
+              <template #error>
+                <div class="repair-image-error">图片加载失败</div>
+              </template>
+            </el-image>
+            <span class="repair-image-item__index">{{ index + 1 }}</span>
+          </div>
+        </div>
+        <div v-else class="repair-image-empty">暂无维修后图片</div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { Camera } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  beforeImages: {
+    type: [String, Array, Object],
+    default: ''
+  },
+  afterImages: {
+    type: [String, Array, Object],
+    default: ''
+  }
+})
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url
+  if (baseUrl && (url === baseUrl || url.startsWith(`${baseUrl}/`))) return url
+  return url.startsWith('/') ? baseUrl + url : `${baseUrl}/${url}`
+}
+
+function toImageList(value) {
+  const values = Array.isArray(value)
+    ? value
+    : typeof value === 'string'
+      ? value.split(',')
+      : value
+        ? [value]
+        : []
+
+  return values
+    .map(item => typeof item === 'string' ? item : (item.url || item.fileName || item.name))
+    .map(normalizeImageUrl)
+    .filter(Boolean)
+}
+
+const beforeImageList = computed(() => toImageList(props.beforeImages))
+const afterImageList = computed(() => toImageList(props.afterImages))
+const allImages = computed(() => beforeImageList.value.concat(afterImageList.value))
+</script>
+
+<style scoped lang="scss">
+.repair-image-panel {
+  margin-top: 20px;
+  padding-top: 16px;
+  border-top: 1px solid #e9ecef;
+}
+
+.repair-image-panel__header,
+.repair-image-column__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.repair-image-panel__header {
+  margin-bottom: 12px;
+}
+
+.repair-image-panel__title {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.repair-image-panel__total {
+  color: #909399;
+  font-size: 13px;
+}
+
+.repair-image-panel__body {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.repair-image-column {
+  min-width: 0;
+  padding: 14px;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  background: #fafbfc;
+}
+
+.repair-image-column__header {
+  margin-bottom: 12px;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.repair-image-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
+  gap: 10px;
+}
+
+.repair-image-item {
+  position: relative;
+  width: 100%;
+  aspect-ratio: 4 / 3;
+  overflow: hidden;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  background: #f2f6fc;
+  cursor: pointer;
+}
+
+.repair-image-item:hover :deep(.el-image__inner) {
+  transform: scale(1.04);
+}
+
+.repair-image-media {
+  width: 100%;
+  height: 100%;
+}
+
+.repair-image-media :deep(.el-image__inner) {
+  transition: transform 0.2s ease;
+}
+
+.repair-image-item__index {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  min-width: 20px;
+  padding: 2px 5px;
+  border-radius: 10px;
+  background: rgb(0 0 0 / 55%);
+  color: #fff;
+  font-size: 12px;
+  line-height: 16px;
+  text-align: center;
+}
+
+.repair-image-empty,
+.repair-image-error {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #a8abb2;
+  font-size: 13px;
+}
+
+.repair-image-empty {
+  min-height: 120px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+}
+
+.repair-image-error {
+  width: 100%;
+  height: 100%;
+}
+
+@media (max-width: 900px) {
+  .repair-image-panel__body {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 71 - 25
src/views/subSystem/basic/BusinessWorkOrder.vue

@@ -234,19 +234,7 @@
                   <el-descriptions-item label="处置结果">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
                 </el-descriptions>
                 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview 
-                      v-for="(img, index) in workImageList" 
-                      :key="index" 
-                      :src="img" 
-                      :width="100" 
-                      :height="100" 
-                    />
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
                 
                 <!-- 工单操作 -->
                 <div class="action-section">
@@ -386,6 +374,7 @@
     
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
+      <ImageUpload v-if="actionTitle === '处理工单'" v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-input v-model="actionFormData.solution" type="textarea" :rows="6" placeholder="请输入说明文字" />
       <template #footer>
         <el-button @click="actionDialogVisible = false">取消</el-button>
@@ -394,7 +383,8 @@
     </el-dialog>
     
     <!-- 完成/验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px">
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-form label-width="80px">
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" />
@@ -418,13 +408,27 @@
         <el-button type="primary" @click="submitClose">确认验收</el-button>
       </template>
     </el-dialog>
+
+    <!-- 驳回工单弹窗 -->
+    <el-dialog v-model="rejectDialogVisible" title="驳回工单" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="驳回原因" required>
+          <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因(驳回后工单状态为已驳回)" maxlength="300" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" :loading="rejecting" @click="submitReject">确认驳回</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="BusinessWorkOrder">
 import { ElMessage } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview'
-import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder } from '@/api/work/order'
+import ImageUpload from '@/components/ImageUpload'
+import RepairImagePanel from '@/components/RepairImagePanel'
+import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, rejectWorkOrder } from '@/api/work/order'
 import { listDept } from '@/api/system/dept'
 import { listUser } from '@/api/system/user'
 import { handleTree } from '@/utils/ruoyi'
@@ -449,12 +453,6 @@ const selectedOrder = ref({})
 const activeTab = ref('info')
 const currentOrderLogs = ref([])
 
-// 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 // 派单相关
 const dispatchDialogVisible = ref(false)
 const dispatchForm = reactive({ handlerIds: [], remarks: '' })
@@ -606,6 +604,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (![2, 3].includes(selectedOrder.value.orderStatus)) {
@@ -614,6 +614,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -624,6 +625,7 @@ function handleProcess() {
   }
   actionTitle.value = '处理工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -638,6 +640,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -655,6 +660,7 @@ async function submitAction() {
 /** 完成/验收相关 */
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (![4, 5].includes(selectedOrder.value.orderStatus)) {
@@ -662,6 +668,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -671,7 +678,11 @@ async function submitComplete() {
     return
   }
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     handleSearch()
@@ -681,13 +692,38 @@ async function submitComplete() {
 }
 
 /** 驳回相关 */
+const rejectDialogVisible = ref(false)
+const rejectReason = ref('')
+const rejecting = ref(false)
+
 function handleReject() {
   if (![1, 2].includes(selectedOrder.value.orderStatus)) {
     ElMessage.warning('当前工单不允许驳回')
     return
   }
-  ElMessage.info('驳回功能待实现')
-  // TODO: 实现驳回逻辑
+  rejectReason.value = ''
+  rejectDialogVisible.value = true
+}
+
+async function submitReject() {
+  if (!rejectReason.value.trim()) {
+    ElMessage.warning('请填写驳回原因')
+    return
+  }
+  rejecting.value = true
+  try {
+    await rejectWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      reason: rejectReason.value.trim()
+    })
+    ElMessage.success('驳回成功,工单状态已更新为已驳回')
+    rejectDialogVisible.value = false
+    handleSearch()
+  } catch (e) {
+    ElMessage.error('驳回失败:' + (e.msg || e.message || '请重试'))
+  } finally {
+    rejecting.value = false
+  }
 }
 
 /** 验收通过/结案相关 */
@@ -916,6 +952,16 @@ onMounted(() => {
     margin-top: 24px;
     padding-top: 24px;
     border-top: 1px solid #e4e7ed;
+
+    .image-group {
+      margin-bottom: 16px;
+    }
+
+    .image-group-title {
+      margin-bottom: 8px;
+      color: #606266;
+      font-weight: 600;
+    }
     
     .image-list {
       display: flex;

+ 71 - 25
src/views/subSystem/drainage/DrainageWorkOrder.vue

@@ -232,19 +232,7 @@
                   <el-descriptions-item label="处置结果">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
                 </el-descriptions>
                 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview 
-                      v-for="(img, index) in workImageList" 
-                      :key="index" 
-                      :src="img" 
-                      :width="100" 
-                      :height="100" 
-                    />
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
                 
                 <!-- 工单操作 -->
                 <div class="action-section">
@@ -384,6 +372,7 @@
     
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
+      <ImageUpload v-if="actionTitle === '处理工单'" v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-input v-model="actionFormData.solution" type="textarea" :rows="6" placeholder="请输入说明文字" />
       <template #footer>
         <el-button @click="actionDialogVisible = false">取消</el-button>
@@ -392,7 +381,8 @@
     </el-dialog>
     
     <!-- 完成/验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px">
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
       <el-form label-width="80px">
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" />
@@ -416,13 +406,27 @@
         <el-button type="primary" @click="submitClose">确认验收</el-button>
       </template>
     </el-dialog>
+
+    <!-- 驳回工单弹窗 -->
+    <el-dialog v-model="rejectDialogVisible" title="驳回工单" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="驳回原因" required>
+          <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因(驳回后工单状态为已驳回)" maxlength="300" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" :loading="rejecting" @click="submitReject">确认驳回</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="DrainageWorkOrder">
 import { ElMessage } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview/index.vue'
-import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder } from '@/api/work/order.js'
+import ImageUpload from '@/components/ImageUpload/index.vue'
+import RepairImagePanel from '@/components/RepairImagePanel/index.vue'
+import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, rejectWorkOrder } from '@/api/work/order.js'
 import { listDept } from '@/api/system/dept.js'
 import { listUser } from '@/api/system/user.js'
 import { handleTree } from '@/utils/ruoyi.js'
@@ -447,12 +451,6 @@ const selectedOrder = ref({})
 const activeTab = ref('info')
 const currentOrderLogs = ref([])
 
-// 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 // 派单相关
 const dispatchDialogVisible = ref(false)
 const dispatchForm = reactive({ handlerIds: [], remarks: '' })
@@ -604,6 +602,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (![2, 3].includes(selectedOrder.value.orderStatus)) {
@@ -612,6 +612,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -622,6 +623,7 @@ function handleProcess() {
   }
   actionTitle.value = '处理工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -636,6 +638,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -652,6 +657,7 @@ async function submitAction() {
 /** 完成/验收相关 */
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (![4, 5].includes(selectedOrder.value.orderStatus)) {
@@ -659,6 +665,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -668,7 +675,11 @@ async function submitComplete() {
     return
   }
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     handleSearch()
@@ -678,13 +689,38 @@ async function submitComplete() {
 }
 
 /** 驳回相关 */
+const rejectDialogVisible = ref(false)
+const rejectReason = ref('')
+const rejecting = ref(false)
+
 function handleReject() {
   if (![1, 2].includes(selectedOrder.value.orderStatus)) {
     ElMessage.warning('当前工单不允许驳回')
     return
   }
-  ElMessage.info('驳回功能待实现')
-  // TODO: 实现驳回逻辑
+  rejectReason.value = ''
+  rejectDialogVisible.value = true
+}
+
+async function submitReject() {
+  if (!rejectReason.value.trim()) {
+    ElMessage.warning('请填写驳回原因')
+    return
+  }
+  rejecting.value = true
+  try {
+    await rejectWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      reason: rejectReason.value.trim()
+    })
+    ElMessage.success('驳回成功,工单状态已更新为已驳回')
+    rejectDialogVisible.value = false
+    handleSearch()
+  } catch (e) {
+    ElMessage.error('驳回失败:' + (e.msg || e.message || '请重试'))
+  } finally {
+    rejecting.value = false
+  }
 }
 
 /** 验收通过/结案相关 */
@@ -913,6 +949,16 @@ onMounted(() => {
     margin-top: 24px;
     padding-top: 24px;
     border-top: 1px solid #e4e7ed;
+
+    .image-group {
+      margin-bottom: 16px;
+    }
+
+    .image-group-title {
+      margin-bottom: 8px;
+      color: #606266;
+      font-weight: 600;
+    }
     
     .image-list {
       display: flex;

+ 13 - 6
src/views/subSystem/drainage/jcsj/sbyx.vue

@@ -634,20 +634,27 @@ function handleRepairSubmit() {
       return
     }
     const orderLevel = urgencyToOrderLevel[repairForm.value.urgency]
-    try {
-      // 批量派单时为每个设备分别创建工单
-      for (const row of rows) {
+    // 批量派单时为每个设备分别创建工单(单个失败不影响其他设备,后端校验:同一设备仅允许一个进行中工单)
+    const failList = []
+    for (const row of rows) {
+      try {
         await addWorkOrder({
           deviceId: String(row.equipmentId),
           orderType: 1,
           orderLevel,
           orderDesc: repairForm.value.faultDesc
         })
+      } catch (error) {
+        failList.push((row.equipmentName || row.equipmentCode || row.equipmentId) + ':' + (error?.message || error?.msg || '创建失败'))
       }
-      ElMessage.success('维修派单成功')
+    }
+    if (failList.length === 0) {
+      ElMessage.success('维修派单成功,共 ' + rows.length + ' 个工单')
       repairDialogVisible.value = false
-    } catch (error) {
-      console.error('维修派单失败', error)
+    } else if (failList.length === rows.length) {
+      ElMessage.error('维修派单失败:' + failList[0])
+    } else {
+      ElMessage.warning('部分设备派单失败(' + failList.length + '/' + rows.length + '):' + failList.join(';'))
     }
   })
 }

+ 202 - 5
src/views/subSystem/manholeCover/caseHandling/orderDisposal.vue

@@ -103,6 +103,8 @@
               <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '—' }}</el-descriptions-item>
             </el-descriptions>
 
+            <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
+
             <div class="mt-5 border-t border-[#e9ecef] pt-3">
               <div class="mb-3 mt-4 flex items-center gap-2 text-[16px] font-600">
                 <span><el-icon><Timer /></el-icon> 工单操作</span>
@@ -134,8 +136,24 @@
                 :timestamp="formatDateTime(log.operTime)"
                 :type="getLogType(log.operType)"
               >
-                {{ log.operDesc }}
-              <span v-if="log.operUserName" class="ml-3 text-[12px] text-[#909399]">操作人: {{ log.operUserName }}</span>
+                <div>
+                  <span>{{ log.operDesc }}</span>
+                  <span v-if="log.operUserName" class="ml-3 text-[12px] text-[#909399]">操作人:{{ log.operUserName }}</span>
+                  <!-- 显示处理图片(如果有的话) -->
+                  <div v-if="log.maintenanceImages && log.maintenanceImages.length" class="mt-2 flex gap-2">
+                    <div
+                      v-for="(img, idx) in log.maintenanceImages"
+                      :key="idx"
+                      class="cursor-pointer rounded border p-1 hover:shadow-md"
+                      @click="() => previewImage(img.url)"
+                    >
+                      <img :src="img.url" alt="处理图片" class="h-[60px] w-[80px] object-cover" />
+                      <div class="text-center text-xs text-[#909399]">
+                        {{ img.type === 'before' ? '⚠️ 处理前' : img.type === 'after' ? '✅ 处理后' : '🖼️ 图片' }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
               </el-timeline-item>
             </el-timeline>
             <el-empty v-else description="暂无工单日志" :image-size="80" />
@@ -148,6 +166,22 @@
       </div>
     </div>
 
+    <el-dialog v-model="beforeImageDialogVisible" title="开始维修" width="600px">
+      <ImageUpload v-model="maintenanceBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+      <template #footer>
+        <el-button @click="beforeImageDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitStartRepair">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="afterImageDialogVisible" title="提交验收" width="600px">
+      <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+      <template #footer>
+        <el-button @click="afterImageDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitVerify">提交</el-button>
+      </template>
+    </el-dialog>
+
     <el-dialog v-model="delayDialogVisible" title="申请延期" width="450px">
       <el-form label-width="88px">
         <el-form-item label="截止时间">
@@ -216,13 +250,20 @@
         <el-button type="primary" @click="confirmDispatchUser">确认选择</el-button>
       </template>
     </el-dialog>
+
+    <!-- 图片预览弹窗 -->
+    <el-dialog v-model="imagePreviewVisible" title="图片预览" width="600px" append-to-body>
+      <img :src="currentImagePreviewUrl" alt="工单图片" class="max-w-full" style="border-radius: 4px;" />
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import {computed, nextTick, onMounted, ref, watch} from 'vue'
+import{computed, nextTick, onMounted, ref, watch} from 'vue'
 import {Document, Search, Tickets, Timer} from '@element-plus/icons-vue'
 import {ElMessage} from 'element-plus'
+import ImageUpload from '@/components/ImageUpload/index.vue'
+import RepairImagePanel from '@/components/RepairImagePanel/index.vue'
 import {getManholeWorkOrderDetail, getManholeWorkOrderPage, operManholeWorkOrder} from '@/api/pipeNetwork/basic'
 import {deptTreeSelect, listUser} from '@/api/system/user'
 
@@ -242,6 +283,11 @@ const receiveUserDisplay = ref('')
 const delayDialogVisible = ref(false)
 const delayDeadline = ref('')
 const delayReason = ref('')
+const beforeImageDialogVisible = ref(false)
+const afterImageDialogVisible = ref(false)
+const maintenanceBeforeImages = ref('')
+const maintenanceAfterImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 const dispatchDialogVisible = ref(false)
 const dispatchUserLoading = ref(false)
 const deptOptions = ref([])
@@ -251,11 +297,24 @@ const dispatchUsers = ref([])
 const dispatchSelectedUser = ref(null)
 const deptTreeRef = ref(null)
 
+// 图片预览相关
+const imagePreviewVisible = ref(false)
+const currentImagePreviewUrl = ref('')
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+
 function formatDateTime(value) {
   if (!value) return '—'
   return String(value).replace('T', ' ')
 }
 
+const maintenanceBeforeImageList = computed(() => splitImageList(selectedOrder.value?.maintenanceBeforeImg))
+const maintenanceAfterImageList = computed(() => splitImageList(selectedOrder.value?.maintenanceAfterImg))
+
+function previewImage(url) {
+  currentImagePreviewUrl.value = url
+  imagePreviewVisible.value = true
+}
+
 function getOrderLevelMeta(level) {
   const map = {
     1: { text: '紧急', tag: 'danger' },
@@ -581,8 +640,27 @@ async function acceptOrder() {
 }
 
 async function startRepair() {
+  maintenanceBeforeImages.value = ''
+  beforeImageDialogVisible.value = true
+}
+
+function normalizeImageUrl(value) {
+  const url = String(value || '').trim()
+  if (!url || /^(https?:|blob:|data:)/.test(url)) return url
+  if (baseUrl && (url === baseUrl || url.startsWith(`${baseUrl}/`))) return url
+  return url.startsWith('/') ? baseUrl + url : `${baseUrl}/${url}`
+}
+
+function splitImageList(value) {
+  return value
+    ? String(value).split(',').map(normalizeImageUrl).filter(Boolean)
+    : []
+}
+
+async function submitStartRepair() {
   try {
-    await operateCurrent(4, '开始维修')
+    await operateCurrent(4, '开始维修', { maintenanceBeforeImg: maintenanceBeforeImages.value })
+    beforeImageDialogVisible.value = false
     ElMessage.success('已开始维修')
   } catch (error) {
     ElMessage.error(error?.message || '开始维修失败')
@@ -590,8 +668,14 @@ async function startRepair() {
 }
 
 async function submitVerify() {
+  if (!afterImageDialogVisible.value) {
+    maintenanceAfterImages.value = ''
+    afterImageDialogVisible.value = true
+    return
+  }
   try {
-    await operateCurrent(5, '提交验收')
+    await operateCurrent(5, '提交验收', { maintenanceAfterImg: maintenanceAfterImages.value })
+    afterImageDialogVisible.value = false
     ElMessage.success('已提交验收')
   } catch (error) {
     ElMessage.error(error?.message || '提交验收失败')
@@ -670,3 +754,116 @@ onMounted(() => {
   loadOrders()
 })
 </script>
+
+<style scoped lang="scss">
+.repair-image-panel {
+  margin-top: 20px;
+  padding-top: 16px;
+  border-top: 1px solid #e9ecef;
+}
+
+.repair-image-panel__header,
+.repair-image-column__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.repair-image-panel__header {
+  margin-bottom: 12px;
+}
+
+.repair-image-panel__title {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.repair-image-panel__total {
+  color: #909399;
+  font-size: 13px;
+}
+
+.repair-image-panel__body {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.repair-image-column {
+  min-width: 0;
+  padding: 14px;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  background: #fafbfc;
+}
+
+.repair-image-column__header {
+  margin-bottom: 12px;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.repair-image-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
+  gap: 10px;
+}
+
+.repair-image-item {
+  position: relative;
+  aspect-ratio: 4 / 3;
+  overflow: hidden;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  background: #f2f6fc;
+  cursor: pointer;
+}
+
+.repair-image-item:hover img {
+  transform: scale(1.04);
+}
+
+.repair-image-item img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  transition: transform 0.2s ease;
+}
+
+.repair-image-item__index {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  min-width: 20px;
+  padding: 2px 5px;
+  border-radius: 10px;
+  background: rgb(0 0 0 / 55%);
+  color: #fff;
+  font-size: 12px;
+  line-height: 16px;
+  text-align: center;
+}
+
+.repair-image-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 120px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+  color: #a8abb2;
+  font-size: 13px;
+}
+
+@media (max-width: 900px) {
+  .repair-image-panel__body {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 9 - 17
src/views/subSystem/pipeNetwork/pworkOrder/Pdayinspect.vue

@@ -129,13 +129,7 @@
                   </el-descriptions>
                 </div>
 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview v-for="(img, index) in workImageList" :key="index" :src="img" :width="100" :height="100"/>
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
 
                 <!-- 工单操作(参考排水/燃气工单按钮组) -->
                 <div class="action-section">
@@ -143,8 +137,12 @@
                   <div class="action-buttons">
                     <el-button v-if="canOperate.dispatch(selectedOrder)" type="primary" @click="handleDispatch">下派任务</el-button>
                     <el-button v-if="canOperate.verify(selectedOrder)" type="warning" @click="handleVerify">核查</el-button>
-                    <el-button v-if="canOperate.process(selectedOrder)" type="success" @click="handleProcess">处理</el-button>
-                    <el-button v-if="canOperate.complete(selectedOrder)" type="danger" @click="handleComplete">提交验收</el-button>
+                    <el-button v-if="canOperate.process(selectedOrder)" type="success" plain @click="handleProcess">
+                      📷 处理(可传处理前照片)
+                    </el-button>
+                    <el-button v-if="canOperate.complete(selectedOrder)" type="danger" plain @click="handleComplete">
+                      ✅ 提交验收(可传维修后照片)
+                    </el-button>
                     <el-button v-if="canOperate.close(selectedOrder)" type="success" @click="handleClose">验收通过</el-button>
                     <el-button v-if="canOperate.cancel(selectedOrder)" type="info" plain @click="handleCancel">取消工单</el-button>
                   </div>
@@ -300,7 +298,7 @@
 <script setup>
 import { ref, reactive, computed, watch, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview'
+import RepairImagePanel from '@/components/RepairImagePanel'
 import { listWorkOrder, getWorkOrderDetail, addWorkOrder, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
 import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
 import { listDept } from '@/api/system/dept'
@@ -352,12 +350,6 @@ const currentOrderLogs = ref([])
 const detailLoading = ref(false)
 const detailDevice = ref(null)
 
-// 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 async function handleSelectOrder(order) {
   selectedOrderId.value = order.orderId
   selectedOrder.value = { ...order }
@@ -599,7 +591,7 @@ async function submitComplete() {
   }
   submitting.value = true
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({ orderId: selectedOrder.value.orderId, result: completeForm.result })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     refreshAfterAction()

+ 55 - 20
src/views/subSystem/pipeNetwork/pworkOrder/PworkList.vue

@@ -1,4 +1,4 @@
-<!--设备故障工单(故障维修 orderType=1,参考排水/燃气工单形态)-->
+<!--设备故障工单(故障维修 orderType=1,参考排水/燃气工单形态)-->
 <!--数据源:/api/work-order/*(orderType=1 过滤;equipmentType 为空 = 所有设备)-->
 <!--需求 5.2.4.4.1:手动新增字段=设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式/故障时间/故障描述-->
 <template>
@@ -6,7 +6,7 @@
     <!-- 顶部搜索区(参考排水/燃气工单) -->
     <el-card class="search-card" shadow="never">
       <div class="search-form">
-        <el-form :model="queryParams" ref="queryFormRef" :inline>
+        <el-form :model="queryParams" ref="queryFormRef" inline>
           <el-form-item label="优先级">
             <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
               <el-option v-for="(v, k) in ORDER_LEVEL_MAP" :key="k" :label="v.text" :value="Number(k)"/>
@@ -136,13 +136,7 @@
                   </el-descriptions>
                 </div>
 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview v-for="(img, index) in workImageList" :key="index" :src="img" :width="100" :height="100"/>
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
 
                 <!-- 工单操作(参考排水/燃气工单按钮组) -->
                 <div class="action-section">
@@ -294,6 +288,9 @@
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
       <el-form label-width="90px">
+        <el-form-item v-if="actionTitle === '处理工单'" label="处理前照片">
+          <ImageUpload v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
         <el-form-item label="结果说明" required>
           <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入故障原因、处理结果及处理措施'" maxlength="500" show-word-limit/>
         </el-form-item>
@@ -305,8 +302,11 @@
     </el-dialog>
 
     <!-- 提交验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px" append-to-body>
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px" append-to-body>
       <el-form label-width="80px">
+        <el-form-item label="维修后照片">
+          <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写维修处置结果说明" maxlength="500" show-word-limit/>
         </el-form-item>
@@ -335,7 +335,7 @@
 <script setup>
 import { ref, reactive, computed, watch, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import ImagePreview from '@/components/ImagePreview'
+import RepairImagePanel from '@/components/RepairImagePanel'
 import { listWorkOrder, getWorkOrderDetail, addWorkOrder, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
 import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
 import { listDept } from '@/api/system/dept'
@@ -389,11 +389,6 @@ const currentOrderLogs = ref([])
 const detailLoading = ref(false)
 const detailDevice = ref(null)
 
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 async function handleSelectOrder(order) {
   selectedOrderId.value = order.orderId
   selectedOrder.value = { ...order }
@@ -599,6 +594,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (!canOperate.verify(selectedOrder.value)) {
@@ -607,6 +604,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -615,9 +613,26 @@ function handleProcess() {
     ElMessage.warning('当前工单不允许处理')
     return
   }
-  actionTitle.value = '处理工单'
-  actionFormData.solution = ''
-  actionDialogVisible.value = true
+
+  ElMessageBox.alert(
+    '<div style="padding: 10px; color: #666;">' +
+    '<strong>维修操作指引:</strong><br/><br/>' +
+    '• 在下方表格中可以上传处理前的照片(最多9张)<br/>' +
+    '• 请选择本地图片或拍照上传<br/>' +
+    '• 填写处理说明后点击"确定"提交<br/>' +
+    '</div>',
+    '开始维修',
+    {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '我已了解',
+      center: false
+    }
+  ).then(() => {
+    actionTitle.value = '处理工单'
+    actionFormData.solution = ''
+    processBeforeImages.value = ''
+    actionDialogVisible.value = true
+  }).catch(() => {})
 }
 
 async function submitAction() {
@@ -632,6 +647,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -647,6 +665,7 @@ async function submitAction() {
 // ========== 提交验收(后端 completeOrder 要求状态 4) ==========
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (!canOperate.complete(selectedOrder.value)) {
@@ -654,6 +673,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -664,7 +684,11 @@ async function submitComplete() {
   }
   submitting.value = true
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     refreshAfterAction()
@@ -900,6 +924,17 @@ onMounted(() => {
     gap: 12px;
   }
 
+  .image-group + .image-group {
+    margin-top: 16px;
+  }
+
+  .image-label {
+    display: block;
+    margin-bottom: 8px;
+    color: #606266;
+    font-size: 13px;
+  }
+
   .action-buttons {
     display: flex;
     gap: 12px;

+ 55 - 19
src/views/subSystem/pipeNetwork/pworkOrder/Pworkdisposition.vue

@@ -1,11 +1,11 @@
-<!--全部工单处置(参考排水/燃气工单:搜索卡 + 左列表 + 右详情 tabs + 按钮操作 + 部门树派单)-->
+<!--全部工单处置(参考排水/燃气工单:搜索卡 + 左列表 + 右详情 tabs + 按钮操作 + 部门树派单)-->
 <!--数据源:/api/work-order/*(equipmentType 为空 = 查询所有设备工单)-->
 <template>
   <div class="order-container">
     <!-- 顶部搜索区(参考排水/燃气工单) -->
     <el-card class="search-card" shadow="never">
       <div class="search-form">
-        <el-form :model="queryParams" ref="queryFormRef" :inline>
+        <el-form :model="queryParams" ref="queryFormRef" inline>
           <el-form-item label="类型">
             <el-select v-model="queryParams.orderType" placeholder="全部类型" clearable style="width: 150px">
               <el-option v-for="(v, k) in ORDER_TYPE_MAP" :key="k" :label="v.text" :value="Number(k)"/>
@@ -137,13 +137,7 @@
                   </el-descriptions>
                 </div>
 
-                <!-- 工单图片 -->
-                <div v-if="workImageList.length > 0" class="image-section">
-                  <h4 class="section-title">工单图片</h4>
-                  <div class="image-list">
-                    <image-preview v-for="(img, index) in workImageList" :key="index" :src="img" :width="100" :height="100"/>
-                  </div>
-                </div>
+                <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
 
                 <!-- 工单操作(参考排水/燃气工单按钮组) -->
                 <div class="action-section">
@@ -248,6 +242,9 @@
     <!-- 核查/处理弹窗(参考排水/燃气工单) -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
       <el-form label-width="90px">
+        <el-form-item v-if="actionTitle === '处理工单'" label="处理前照片">
+          <ImageUpload v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
         <el-form-item label="结果说明" required>
           <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入故障原因、处理结果及处理措施'" maxlength="500" show-word-limit/>
         </el-form-item>
@@ -259,8 +256,11 @@
     </el-dialog>
 
     <!-- 提交验收弹窗 -->
-    <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px" append-to-body>
+    <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px" append-to-body>
       <el-form label-width="80px">
+        <el-form-item label="维修后照片">
+          <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
+        </el-form-item>
         <el-form-item label="验收意见">
           <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写维修处置结果说明" maxlength="500" show-word-limit/>
         </el-form-item>
@@ -289,6 +289,7 @@
 <script setup>
 import { ref, reactive, computed, watch, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import RepairImagePanel from '@/components/RepairImagePanel'
 import ImagePreview from '@/components/ImagePreview'
 import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
 import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
@@ -344,11 +345,6 @@ const detailLoading = ref(false)
 const detailDevice = ref(null)
 
 // 工单图片列表(多个用逗号分隔)
-const workImageList = computed(() => {
-  if (!selectedOrder.value.workImage) return []
-  return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
-})
-
 async function handleSelectOrder(order) {
   selectedOrderId.value = order.orderId
   selectedOrder.value = { ...order }
@@ -492,6 +488,8 @@ async function submitDispatch() {
 const actionDialogVisible = ref(false)
 const actionTitle = ref('')
 const actionFormData = reactive({ solution: '' })
+const processBeforeImages = ref('')
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
 
 function handleVerify() {
   if (!canOperate.verify(selectedOrder.value)) {
@@ -500,6 +498,7 @@ function handleVerify() {
   }
   actionTitle.value = '核查工单'
   actionFormData.solution = ''
+  processBeforeImages.value = ''
   actionDialogVisible.value = true
 }
 
@@ -508,9 +507,26 @@ function handleProcess() {
     ElMessage.warning('当前工单不允许处理')
     return
   }
-  actionTitle.value = '处理工单'
-  actionFormData.solution = ''
-  actionDialogVisible.value = true
+
+  ElMessageBox.alert(
+    '<div style="padding: 10px; color: #666;">' +
+    '<strong>维修操作指引:</strong><br/><br/>' +
+    '• 在下方表格中可以上传处理前的照片(最多9张)<br/>' +
+    '• 请选择本地图片或拍照上传<br/>' +
+    '• 填写处理说明后点击"确定"提交<br/>' +
+    '</div>',
+    '开始维修',
+    {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '我已了解',
+      center: false
+    }
+  ).then(() => {
+    actionTitle.value = '处理工单'
+    actionFormData.solution = ''
+    processBeforeImages.value = ''
+    actionDialogVisible.value = true
+  }).catch(() => {})
 }
 
 async function submitAction() {
@@ -525,6 +541,9 @@ async function submitAction() {
       handleResult: 'solved',
       solution: actionFormData.solution
     }
+    if (actionTitle.value === '处理工单' && processBeforeImages.value) {
+      data.maintenanceBeforeImg = processBeforeImages.value
+    }
     if (actionTitle.value === '核查工单') {
       await verifyWorkOrder(data)
     } else {
@@ -541,6 +560,7 @@ async function submitAction() {
 // ========== 提交验收(后端 completeOrder 要求状态 4) ==========
 const completeDialogVisible = ref(false)
 const completeForm = reactive({ result: '' })
+const maintenanceAfterImages = ref('')
 
 function handleComplete() {
   if (!canOperate.complete(selectedOrder.value)) {
@@ -548,6 +568,7 @@ function handleComplete() {
     return
   }
   completeForm.result = ''
+  maintenanceAfterImages.value = ''
   completeDialogVisible.value = true
 }
 
@@ -558,7 +579,11 @@ async function submitComplete() {
   }
   submitting.value = true
   try {
-    await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
+    await completeWorkOrder({
+      orderId: selectedOrder.value.orderId,
+      result: completeForm.result,
+      maintenanceAfterImg: maintenanceAfterImages.value
+    })
     ElMessage.success('提交验收成功')
     completeDialogVisible.value = false
     handleSearch()
@@ -788,6 +813,17 @@ onMounted(() => {
     gap: 12px;
   }
 
+  .image-group + .image-group {
+    margin-top: 16px;
+  }
+
+  .image-label {
+    display: block;
+    margin-bottom: 8px;
+    color: #606266;
+    font-size: 13px;
+  }
+
   .action-buttons {
     display: flex;
     gap: 12px;