buhuazhen
15 小时以前 3c590674cb2b134c676e356d7e5ecef18b1a4399
feat 设备巡检 调整为一次性完成
已修改2个文件
387 ■■■■ 文件已修改
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 220 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 167 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
@@ -6,15 +6,9 @@
      width="560px"
      :before-close="closeUploadDialog"
    >
      <el-tabs v-model="currentUploadType">
        <el-tab-pane label="生产前" name="before" />
        <el-tab-pane label="生产中" name="after" />
        <el-tab-pane label="生产后" name="issue" />
      </el-tabs>
      <div class="exception-section">
        <div class="section-title">是否存在异常?</div>
        <el-radio-group v-model="currentHasException">
        <el-radio-group v-model="hasException">
          <el-radio :value="false">正常</el-radio>
          <el-radio :value="true">存在异常</el-radio>
        </el-radio-group>
@@ -33,12 +27,12 @@
          :on-success="handleUploadSuccess"
          :on-error="handleUploadError"
          :on-progress="handleUploadProgress"
          :disabled="uploading || getCurrentFiles().length >= uploadConfig.limit"
          :disabled="uploading || fileList.length >= uploadConfig.limit"
        >
          <el-button
            type="primary"
            :loading="uploading"
            :disabled="getCurrentFiles().length >= uploadConfig.limit"
            :disabled="fileList.length >= uploadConfig.limit"
          >
            选择图片/视频
          </el-button>
@@ -51,9 +45,9 @@
        style="margin: 12px 0"
      />
      <div v-if="getCurrentFiles().length" class="file-list">
      <div v-if="fileList.length" class="file-list">
        <div
          v-for="(file, index) in getCurrentFiles()"
          v-for="(file, index) in fileList"
          :key="file.uid || file.id || index"
          class="file-item"
        >
@@ -86,17 +80,15 @@
      <el-empty
        v-else
        :description="`请选择要上传的${getUploadTypeText()}图片或视频`"
        description="请选择要上传的图片或视频"
      />
      <div class="upload-summary">
        生产前:{{ beforeModelValue.length }} 个 |
        生产中:{{ afterModelValue.length }} 个 |
        生产后:{{ issueModelValue.length }} 个
        已上传 {{ fileList.length }} 个文件
      </div>
      <template #footer>
                <el-button type="primary" @click="submitUpload">提交</el-button>
        <el-button type="primary" @click="submitUpload">提交</el-button>
        <el-button @click="closeUploadDialog">取消</el-button>
      </template>
    </el-dialog>
@@ -131,31 +123,14 @@
const uploadRef = ref(null);
const uploadFileList = ref([]);
const beforeModelValue = ref([]);
const afterModelValue = ref([]);
const issueModelValue = ref([]);
const currentUploadType = ref("before");
const hasExceptionBefore = ref(null);
const hasExceptionAfter = ref(null);
const hasExceptionIssue = ref(null);
const fileList = ref([]);
const hasException = ref(null);
const currentTask = ref(null);
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
// 根据当前 Tab 获取/设置对应的异常状态
const currentHasException = computed({
  get: () => {
    if (currentUploadType.value === "before") return hasExceptionBefore.value;
    if (currentUploadType.value === "after") return hasExceptionAfter.value;
    return hasExceptionIssue.value;
  },
  set: (val) => {
    if (currentUploadType.value === "before") hasExceptionBefore.value = val;
    else if (currentUploadType.value === "after") hasExceptionAfter.value = val;
    else hasExceptionIssue.value = val;
  }
});
const INSPECTION_TYPE = 10;
const uploadConfig = {
  action: "/file/upload",
@@ -165,8 +140,7 @@
};
const uploadUrl = computed(() => {
  const type = getTabType();
  return `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}?type=${type}`;
  return `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}?type=${INSPECTION_TYPE}`;
});
const uploadHeaders = {
  Authorization: `Bearer ${getToken()}`,
@@ -207,7 +181,7 @@
  return fileUrl;
};
const mapExistingFile = (file, type) => ({
const mapExistingFile = (file) => ({
  ...file,
  id: file?.id,
  tempId: file?.tempId ?? file?.tempFileId ?? file?.id,
@@ -223,18 +197,13 @@
  size: file?.size || file?.byteSize,
  byteSize: file?.byteSize || file?.size,
  contentType: file?.contentType || "",
  type,
  uid: file?.uid || `${type}-${file?.id || file?.url || Math.random()}`,
  type: INSPECTION_TYPE,
  uid: file?.uid || `${INSPECTION_TYPE}-${file?.id || file?.url || Math.random()}`,
});
const resetDialogState = () => {
  beforeModelValue.value = [];
  afterModelValue.value = [];
  issueModelValue.value = [];
  currentUploadType.value = "before";
  hasExceptionBefore.value = null;
  hasExceptionAfter.value = null;
  hasExceptionIssue.value = null;
  fileList.value = [];
  hasException.value = null;
  currentTask.value = null;
  uploadProgress.value = 0;
  uploading.value = false;
@@ -250,23 +219,12 @@
    storageBlobDTO: [],
  };
  beforeModelValue.value = Array.isArray(rawTask.commonFileListBefore)
    ? rawTask.commonFileListBefore.map(file => mapExistingFile(file, 10))
    : [];
  afterModelValue.value = Array.isArray(rawTask.commonFileListAfter)
    ? rawTask.commonFileListAfter.map(file => mapExistingFile(file, 11))
    : [];
  issueModelValue.value = Array.isArray(rawTask.commonFileList)
    ? rawTask.commonFileList.map(file => mapExistingFile(file, 12))
  fileList.value = Array.isArray(rawTask.commonFileList)
    ? rawTask.commonFileList.map(file => mapExistingFile(file))
    : [];
  currentUploadType.value = "before";
  hasExceptionBefore.value =
    typeof rawTask.hasExceptionBefore === "boolean" ? rawTask.hasExceptionBefore : null;
  hasExceptionAfter.value =
    typeof rawTask.hasExceptionAfter === "boolean" ? rawTask.hasExceptionAfter : null;
  hasExceptionIssue.value =
    typeof rawTask.hasExceptionIssue === "boolean" ? rawTask.hasExceptionIssue : null;
  hasException.value =
    typeof rawTask.hasException === "boolean" ? rawTask.hasException : null;
  uploadFileList.value = [];
  showUploadDialog.value = true;
};
@@ -277,26 +235,8 @@
  emit("closeDia");
};
const getCurrentFiles = () => {
  if (currentUploadType.value === "before") return beforeModelValue.value;
  if (currentUploadType.value === "after") return afterModelValue.value;
  return issueModelValue.value;
};
const getUploadTypeText = () => {
  if (currentUploadType.value === "before") return "生产前";
  if (currentUploadType.value === "after") return "生产中";
  return "生产后";
};
const getTabType = () => {
  if (currentUploadType.value === "before") return 10;
  if (currentUploadType.value === "after") return 11;
  return 12;
};
const handleBeforeUpload = file => {
  if (getCurrentFiles().length >= uploadConfig.limit) {
  if (fileList.value.length >= uploadConfig.limit) {
    ElMessage.warning(`最多只能选择${uploadConfig.limit}个文件`);
    return false;
  }
@@ -336,7 +276,6 @@
    return;
  }
  const type = getTabType();
  const objectUrl = file?.raw ? URL.createObjectURL(file.raw) : "";
  const fileData = {
    id: uploadedFile.id,
@@ -354,18 +293,11 @@
    byteSize: uploadedFile.byteSize || uploadedFile.size || file.size,
    createTime: uploadedFile.createTime || Date.now(),
    contentType: uploadedFile.contentType || file.raw?.type || "",
    type,
    type: INSPECTION_TYPE,
    uid: `${Date.now()}-${Math.random()}`,
  };
  if (currentUploadType.value === "before") {
    beforeModelValue.value.push(fileData);
  } else if (currentUploadType.value === "after") {
    afterModelValue.value.push(fileData);
  } else {
    issueModelValue.value.push(fileData);
  }
  fileList.value.push(fileData);
  ElMessage.success("上传成功");
};
@@ -382,52 +314,9 @@
    await ElMessageBox.confirm("确定要删除这个文件吗?", "确认删除", {
      type: "warning",
    });
    if (currentUploadType.value === "before") {
      beforeModelValue.value.splice(index, 1);
    } else if (currentUploadType.value === "after") {
      afterModelValue.value.splice(index, 1);
    } else {
      issueModelValue.value.splice(index, 1);
    }
    fileList.value.splice(index, 1);
    ElMessage.success("删除成功");
  } catch {}
};
const buildSubmitFiles = () => {
  const list = [
    ...beforeModelValue.value,
    ...afterModelValue.value,
    ...issueModelValue.value,
  ];
  return list.map(item => ({
    ...item,
    url: item?.downloadUrl || item?.url || "",
  }));
};
const buildSubmitFileItem = item => {
  if (!item) return null;
  return {
    id: item.id,
    tempId: item.tempId,
    tempFileId: item.tempFileId,
    type: item.type,
    url: item?.downloadUrl || item?.url || "",
    downloadUrl: item?.downloadUrl || item?.url || "",
    bucketFilename: item.bucketFilename,
    originalFilename: item.originalFilename,
    size: item.size,
    byteSize: item.byteSize,
    contentType: item.contentType,
  };
};
const buildGroupedFiles = list => {
  return (list || []).map(buildSubmitFileItem).filter(Boolean);
};
const buildSubmitPayload = () => {
@@ -442,35 +331,46 @@
    ...rest
  } = currentTask.value || {};
  const files = buildSubmitFiles();
  const files = fileList.value.map(item => ({
    ...item,
    url: item?.downloadUrl || item?.url || "",
  }));
  const tempFileIds = files
    .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
    .filter(Boolean);
  return {
    ...rest,
    hasExceptionBefore: hasExceptionBefore.value,
    hasExceptionAfter: hasExceptionAfter.value,
    hasExceptionIssue: hasExceptionIssue.value,
    hasException: hasException.value,
    tempFileIds,
    commonFileListBefore: buildGroupedFiles(beforeModelValue.value),
    commonFileListAfter: buildGroupedFiles(afterModelValue.value),
    commonFileList: buildGroupedFiles(issueModelValue.value),
    commonFileList: files.map(item => {
      if (!item) return null;
      return {
        id: item.id,
        tempId: item.tempId,
        tempFileId: item.tempFileId,
        type: item.type,
        url: item?.downloadUrl || item?.url || "",
        downloadUrl: item?.downloadUrl || item?.url || "",
        bucketFilename: item.bucketFilename,
        originalFilename: item.originalFilename,
        size: item.size,
        byteSize: item.byteSize,
        contentType: item.contentType,
      };
    }).filter(Boolean),
  };
};
const submitUpload = async () => {
  // 只验证当前 Tab 的异常状态
  const currentException = currentHasException.value;
  if (currentException === null) {
    ElMessage.warning(`请选择${getUploadTypeText()}是否存在异常`);
  if (hasException.value === null) {
    ElMessage.warning("请选择是否存在异常");
    return;
  }
  const currentFiles = getCurrentFiles();
  // 只有选择"存在异常"时才需要上传文件
  if (currentException === true && !currentFiles.length) {
    ElMessage.warning(`${getUploadTypeText()}存在异常时请上传图片或视频`);
  if (hasException.value === true && !fileList.value.length) {
    ElMessage.warning("存在异常时请上传图片或视频");
    return;
  }
@@ -481,7 +381,6 @@
  try {
    const payload = buildSubmitPayload();
    const result = await uploadInspectionTask(payload);
    if (result?.code === 200 || result?.success) {
      ElMessage.success("提交成功");
@@ -497,23 +396,6 @@
  } finally {
    loadingInstance.close();
  }
};
const goToRepair = () => {
  const taskInfo = {
    taskId: currentTask.value?.taskId || currentTask.value?.id,
    taskName: currentTask.value?.taskName,
    inspectionLocation: currentTask.value?.inspectionLocation,
    inspector: currentTask.value?.inspector,
    uploadedFiles: {
      before: beforeModelValue.value,
      after: afterModelValue.value,
      issue: issueModelValue.value,
    },
  };
  sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskInfo));
  window.location.href = "/equipmentManagement/repair";
};
const previewAttachment = file => {
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -3,77 +3,20 @@
    <el-dialog title="查看附件"
               v-model="dialogVisitable" width="800px" @close="cancel">
      <div class="upload-container">
        <!-- 生产前 -->
        <div class="form-container">
          <div class="title">生产前</div>
          <!-- 图片列表 -->
          <div class="title">巡检附件</div>
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in beforeProductionImgs" :key="index"
                 @click="showMedia(beforeProductionImgs, index, 'image')"
            <img v-for="(item, index) in images" :key="index"
                 @click="showMedia(images, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <!-- 视频列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
                v-for="(videoUrl, index) in beforeProductionVideos"
                v-for="(videoUrl, index) in videos"
                :key="index"
                @click="showMedia(beforeProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
        <!-- 生产中 -->
        <div class="form-container">
          <div class="title">生产中</div>
          <!-- 图片列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in afterProductionImgs" :key="index"
                 @click="showMedia(afterProductionImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <!-- 视频列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
                v-for="(videoUrl, index) in afterProductionVideos"
                :key="index"
                @click="showMedia(afterProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
        <!-- 生产后 -->
        <div class="form-container">
          <div class="title">生产后</div>
          <!-- 图片列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in productionIssuesImgs" :key="index"
                 @click="showMedia(productionIssuesImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <!-- 视频列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
                v-for="(videoUrl, index) in productionIssuesVideos"
                :key="index"
                @click="showMedia(productionIssuesVideos, index, 'video')"
                @click="showMedia(videos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
@@ -85,11 +28,9 @@
        </div>
      </div>
    </el-dialog>
    <!-- 统一媒体查看器 -->
    <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer">
      <div class="media-viewer-content" @click.stop>
        <!-- 图片 -->
        <vue-easy-lightbox
            v-if="mediaType === 'image'"
            :visible="isMediaViewerVisible"
@@ -97,8 +38,7 @@
            :index="currentMediaIndex"
            @hide="closeMediaViewer"
        ></vue-easy-lightbox>
        <!-- 视频 -->
        <div v-else-if="mediaType === 'video'" style="position: relative;">
          <video
              :src="mediaList[currentMediaIndex]"
@@ -116,114 +56,81 @@
import VueEasyLightbox from 'vue-easy-lightbox';
const { proxy } = getCurrentInstance();
// 控制弹窗显示
const dialogVisitable = ref(false);
// 图片数组
const beforeProductionImgs = ref([]);
const afterProductionImgs = ref([]);
const productionIssuesImgs = ref([]);
const images = ref([]);
const videos = ref([]);
// 视频数组
const beforeProductionVideos = ref([]);
const afterProductionVideos = ref([]);
const productionIssuesVideos = ref([]);
// 媒体查看器状态
const isMediaViewerVisible = ref(false);
const currentMediaIndex = ref(0);
const mediaList = ref([]); // 存储当前要查看的媒体列表(含图片和视频对象)
const mediaType = ref('image'); // image | video
const mediaList = ref([]);
const mediaType = ref('image');
const javaApi = proxy.javaApi;
// 处理 URL:将 Windows 路径转换为可访问的 URL
function processFileUrl(fileUrl) {
  if (!fileUrl) return '';
  // 如果 URL 是 Windows 路径格式(包含反斜杠),需要转换
  if (fileUrl && fileUrl.indexOf('\\') > -1) {
    // 查找 uploads 关键字的位置,从那里开始提取相对路径
    const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads');
    if (uploadsIndex > -1) {
      // 从 uploads 开始提取路径,并将反斜杠替换为正斜杠
      const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/');
      fileUrl = '/' + relativePath;
    } else {
      // 如果没有找到 uploads,提取最后一个目录和文件名
      const parts = fileUrl.split('\\');
      const fileName = parts[parts.length - 1];
      fileUrl = '/uploads/' + fileName;
    }
  }
  // 确保所有非 http 开头的 URL 都拼接 baseUrl
  if (fileUrl && !fileUrl.startsWith('http')) {
    // 确保路径以 / 开头
    if (!fileUrl.startsWith('/')) {
      fileUrl = '/' + fileUrl;
    }
    // 拼接 baseUrl
    fileUrl = javaApi + fileUrl;
  }
  return fileUrl;
}
// 处理每一类数据:分离图片和视频
function processItems(items) {
  const images = [];
  const videos = [];
  // 检查 items 是否存在且为数组
  const imgList = [];
  const vidList = [];
  if (!items || !Array.isArray(items)) {
    return { images, videos };
    return { images: imgList, videos: vidList };
  }
  items.forEach(item => {
    if (!item || !item.url) return;
    // 处理文件 URL
    const fileUrl = processFileUrl(item.url);
    // 根据文件扩展名判断是图片还是视频
    const urlLower = fileUrl.toLowerCase();
    if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) {
      images.push(fileUrl);
      imgList.push(fileUrl);
    } else if (urlLower.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/)) {
      videos.push(fileUrl);
      vidList.push(fileUrl);
    } else if (item.contentType) {
      // 如果有 contentType,使用 contentType 判断
      if (item.contentType.startsWith('image/')) {
        images.push(fileUrl);
        imgList.push(fileUrl);
      } else if (item.contentType.startsWith('video/')) {
        videos.push(fileUrl);
        vidList.push(fileUrl);
      }
    }
  });
  return { images, videos };
  return { images: imgList, videos: vidList };
}
// 打开弹窗并加载数据
const openDialog = async (row) => {
  // 使用正确的字段名:commonFileListBefore, commonFileListAfter, commonFileList
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBefore || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfter || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.commonFileList || []);
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
  afterProductionImgs.value = afterImgs;
  afterProductionVideos.value = afterVids;
  productionIssuesImgs.value = issueImgs;
  productionIssuesVideos.value = issueVids;
  const { images: imgList, videos: vidList } = processItems(row.commonFileList || []);
  images.value = imgList;
  videos.value = vidList;
  dialogVisitable.value = true;
};
// 显示媒体(图片 or 视频)
function showMedia(mediaArray, index, type) {
  mediaList.value = mediaArray;
  currentMediaIndex.value = index;
@@ -231,14 +138,12 @@
  isMediaViewerVisible.value = true;
}
// 关闭媒体查看器
function closeMediaViewer() {
  isMediaViewerVisible.value = false;
  mediaList.value = [];
  mediaType.value = 'image';
}
// 表单关闭方法
const cancel = () => {
  dialogVisitable.value = false;
};
@@ -253,7 +158,7 @@
  padding: 20px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  .form-container {
    flex: 1;
    width: 100%;
@@ -269,7 +174,7 @@
  padding-left: 10px;
  position: relative;
  margin: 6px 0;
  &::before {
    content: "";
    position: absolute;