gaoluyang
2 天以前 3381840f164396afbb2cc6ab395a5cb2854aa8a5
进销存pro
1.web端设备巡检要求也可以上传图片
已添加1个文件
已修改2个文件
1042 ■■■■ 文件已修改
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 680 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,680 @@
<template>
  <FormDialog
    v-model="dialogVisible"
    title="上传巡检记录"
    width="980px"
    @close="handleClose"
    @cancel="handleClose"
  >
    <main class="upload-content">
      <el-card v-if="taskInfo" class="section-card">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="巡检任务名称">
            {{ taskInfo.taskName || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="巡检项目">
            {{ taskInfo.inspectionProject || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="备注">
            {{ taskInfo.remarks || "-" }}
          </el-descriptions-item>
        </el-descriptions>
      </el-card>
      <el-card class="section-card">
        <h3>巡检状态</h3>
        <el-radio-group v-model="hasException">
          <el-radio-button :value="false">正常</el-radio-button>
          <el-radio-button :value="true">存在异常</el-radio-button>
        </el-radio-group>
      </el-card>
      <el-card v-if="hasException === true" class="section-card">
        <h3>异常描述</h3>
        <el-input
          v-model="abnormalDescription"
          type="textarea"
          maxlength="500"
          show-word-limit
          :rows="4"
          placeholder="请描述异常情况..."
        />
      </el-card>
      <el-card v-if="hasException === true" class="section-card">
        <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="upload-buttons">
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="getCurrentFiles().length >= uploadConfig.limit || uploading"
            accept="image/*"
          >
            <el-button type="primary" :loading="uploading">
              <el-icon><Camera /></el-icon>
              é€‰æ‹©å›¾ç‰‡
            </el-button>
          </el-upload>
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="getCurrentFiles().length >= uploadConfig.limit || uploading"
            accept="video/*"
          >
            <el-button type="success" :loading="uploading">
              <el-icon><VideoCamera /></el-icon>
              é€‰æ‹©è§†é¢‘
            </el-button>
          </el-upload>
        </div>
        <el-progress
          v-if="uploading"
          :percentage="uploadProgress"
          class="upload-progress"
        />
        <div v-if="getCurrentFiles().length" class="file-list">
          <div
            v-for="(file, index) in getCurrentFiles()"
            :key="file.uid || file.id || index"
            class="file-item"
          >
            <div class="file-preview-container">
              <el-image
                v-if="file.type === 'image' || !file.type"
                :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                fit="cover"
                class="file-preview"
                :preview-src-list="[file.url || file.tempFilePath || file.path || file.downloadUrl]"
                preview-teleported
              />
              <div v-else class="video-preview" @click="previewVideo(file)">
                <el-icon><VideoCamera /></el-icon>
                <span>视频</span>
              </div>
              <el-button
                class="delete-btn"
                type="danger"
                circle
                size="small"
                @click="removeFile(index)"
              >
                <el-icon><Close /></el-icon>
              </el-button>
            </div>
            <div class="file-info">
              <div class="file-name">
                {{ file.bucketFilename || file.name || (file.type === "image" ? "图片" : "视频") }}
              </div>
              <div class="file-size">{{ formatFileSize(file.size) }}</div>
            </div>
          </div>
        </div>
        <el-empty
          v-else
          :description="`请选择要上传的${getUploadTypeText()}图片或视频`"
        />
        <el-alert
          class="upload-summary"
          type="info"
          :closable="false"
          :title="`生产前:${beforeModelValue.length}个文件 | ç”Ÿäº§ä¸­ï¼š${afterModelValue.length}个文件 | ç”Ÿäº§åŽï¼š${issueModelValue.length}个文件`"
        />
      </el-card>
      <el-result
        v-if="hasException === false"
        icon="success"
        title="设备运行正常"
        sub-title="无需上传照片"
      />
    </main>
    <template #footer>
      <footer class="footer-buttons">
        <el-button type="primary" @click="submitUpload">提交</el-button>
        <el-button v-if="hasException === true" type="warning" @click="goToRepair">
          æ–°å¢žæŠ¥ä¿®
        </el-button>
        <el-button @click="handleClose">取消</el-button>
      </footer>
    </template>
  </FormDialog>
  <el-dialog
    v-model="showVideoDialog"
    :title="currentVideoFile?.originalFilename || currentVideoFile?.name || '视频预览'"
    width="720px"
  >
    <video
      v-if="currentVideoFile"
      :src="currentVideoFile.url || currentVideoFile.downloadUrl"
      class="video-player"
      controls
      autoplay
    />
  </el-dialog>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { Camera, Close, VideoCamera } from "@element-plus/icons-vue";
import axios from "axios";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { uploadInspectionTask } from "@/api/inspectionManagement/index.js";
import { getToken } from "@/utils/auth";
const emit = defineEmits(["closeDia", "success"]);
const router = useRouter();
const dialogVisible = ref(false);
const taskInfo = ref(null);
const uploading = ref(false);
const uploadProgress = ref(0);
const beforeModelValue = ref([]);
const afterModelValue = ref([]);
const issueModelValue = ref([]);
const currentUploadType = ref("before");
const hasException = ref(null);
const abnormalDescription = ref("");
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
const uploadConfig = {
  action: "/common/upload",
  limit: 10,
  fileSize: 50,
  fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
};
const uploadFileUrl = computed(
  () => `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}`
);
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
    }
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
const normalizeList = (list, fileType) => {
  if (!Array.isArray(list)) return [];
  return list.filter(Boolean).map(item => {
    let currentType = item.type;
    if (!currentType && item.contentType) {
      currentType = item.contentType.startsWith("video") ? "video" : "image";
    } else if (!currentType) {
      currentType = fileType || "image";
    }
    return {
      ...item,
      url: processFileUrl(item.url || item.previewURL || item.downloadUrl || item.path || ""),
      downloadUrl: processFileUrl(
        item.downloadUrl || item.url || item.previewURL || item.path || ""
      ),
      name: item.name || item.originalFilename || item.bucketFilename,
      tempId: item.tempId || item.id || item.tempFileId,
      tempFileId: item.tempFileId || item.tempId || item.id,
      size: item.size || item.byteSize || 0,
      type: currentType,
      status: "success",
      uid: item.uid || `${Date.now()}-${Math.random()}`,
    };
  });
};
const resetState = () => {
  taskInfo.value = null;
  beforeModelValue.value = [];
  afterModelValue.value = [];
  issueModelValue.value = [];
  currentUploadType.value = "before";
  hasException.value = null;
  abnormalDescription.value = "";
  uploading.value = false;
  uploadProgress.value = 0;
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
const openDialog = row => {
  const raw = JSON.parse(JSON.stringify(row?.__raw || row || {}));
  taskInfo.value = raw;
  beforeModelValue.value = normalizeList(
    raw.commonFileListBeforeVO || raw.commonFileListBefore || [],
    "image"
  );
  afterModelValue.value = normalizeList(
    raw.commonFileListVO || raw.commonFileList || [],
    "image"
  );
  issueModelValue.value = normalizeList(
    raw.commonFileListAfterVO || raw.commonFileListAfter || [],
    "image"
  );
  abnormalDescription.value = raw.abnormalDescription || "";
  if (raw.hasException !== undefined && raw.hasException !== null) {
    hasException.value = raw.hasException;
  } else if (raw.inspectionResult !== undefined && raw.inspectionResult !== null) {
    hasException.value = String(raw.inspectionResult) === "0";
  } else {
    hasException.value = null;
  }
  if (
    hasException.value !== true &&
    (beforeModelValue.value.length || afterModelValue.value.length || issueModelValue.value.length)
  ) {
    hasException.value = true;
  }
  dialogVisible.value = true;
};
const handleClose = () => {
  dialogVisible.value = false;
  resetState();
  emit("closeDia");
};
const getCurrentFiles = () => {
  if (currentUploadType.value === "before") return beforeModelValue.value;
  if (currentUploadType.value === "after") return afterModelValue.value;
  if (currentUploadType.value === "issue") return issueModelValue.value;
  return [];
};
const getUploadTypeText = () => {
  if (currentUploadType.value === "before") return "生产前";
  if (currentUploadType.value === "after") return "生产中";
  if (currentUploadType.value === "issue") return "生产后";
  return "";
};
const getTabType = () => {
  if (currentUploadType.value === "before") return 10;
  if (currentUploadType.value === "after") return 11;
  if (currentUploadType.value === "issue") return 12;
  return 10;
};
const previewVideo = file => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
const uploadFile = async uploadRequest => {
  const rawFile = uploadRequest.file;
  if (getCurrentFiles().length >= uploadConfig.limit) {
    ElMessage.warning(`最多只能选择${uploadConfig.limit}个文件`);
    return;
  }
  const ext = rawFile.name.split(".").pop()?.toLowerCase();
  if (!uploadConfig.fileType.includes(ext)) {
    ElMessage.warning(`文件格式不支持,请上传 ${uploadConfig.fileType.join("/")} æ ¼å¼`);
    return;
  }
  if (rawFile.size > uploadConfig.fileSize * 1024 * 1024) {
    ElMessage.warning(`文件大小不能超过 ${uploadConfig.fileSize}MB`);
    return;
  }
  const token = getToken();
  if (!token) {
    ElMessage.warning("用户未登录");
    return;
  }
  const formData = new FormData();
  formData.append("files", rawFile);
  formData.append("type", getTabType());
  uploading.value = true;
  uploadProgress.value = 0;
  try {
    const { data } = await axios.post(uploadFileUrl.value, formData, {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: event => {
        if (event.total) {
          uploadProgress.value = Math.round((event.loaded / event.total) * 100);
        }
      },
    });
    if (data.code !== 200) {
      ElMessage.error(data.msg || "上传失败");
      return;
    }
    const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
    const finalUrl = processFileUrl(
      resultData.url || resultData.previewURL || resultData.downloadUrl || ""
    );
    const finalName = resultData.name || resultData.originalFilename || resultData.bucketFilename;
    const finalId = resultData.tempId || resultData.id || resultData.tempFileId;
    const uploadedFile = {
      ...resultData,
      url: finalUrl,
      downloadUrl: finalUrl,
      name: finalName,
      tempId: finalId,
      tempFileId: resultData.tempFileId || finalId,
      size: rawFile.size || resultData.size || resultData.byteSize || 0,
      type: rawFile.type?.startsWith("video") ? "video" : "image",
      status: "success",
      uid: `${Date.now()}-${Math.random()}`,
    };
    getCurrentFiles().push(uploadedFile);
    ElMessage.success("上传成功");
  } catch (error) {
    ElMessage.error(error?.message || "上传失败");
  } finally {
    uploading.value = false;
  }
};
const buildFileItem = item => ({
  id: item?.id,
  tempId: item?.tempId,
  tempFileId: item?.tempFileId,
  url: item?.downloadUrl || item?.url || "",
  downloadUrl: item?.downloadUrl || item?.url || "",
  name: item?.name,
  bucketFilename: item?.bucketFilename || item?.name,
  originalFilename: item?.originalFilename || item?.name,
  size: item?.size || 0,
  byteSize: item?.byteSize || item?.size || 0,
  contentType: item?.contentType || "",
  type: item?.type,
});
const submitUpload = async () => {
  if (hasException.value === null) {
    ElMessage.warning("请选择巡检状态");
    return;
  }
  if (hasException.value === true) {
    const totalFiles =
      beforeModelValue.value.length +
      afterModelValue.value.length +
      issueModelValue.value.length;
    if (!totalFiles) {
      ElMessage.warning("请上传异常照片或视频");
      return;
    }
    if (!abnormalDescription.value.trim()) {
      ElMessage.warning("请填写异常描述");
      return;
    }
  }
  const loading = ElLoading.service({
    text: "提交中...",
    background: "rgba(0, 0, 0, 0.3)",
  });
  try {
    const allFiles = [
      ...beforeModelValue.value,
      ...afterModelValue.value,
      ...issueModelValue.value,
    ];
    const tempFileIds = allFiles
      .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
      .filter(Boolean);
    const {
      createTime,
      updateTime,
      storageBlobDTO,
      commonFileListAfterVO,
      commonFileListVO,
      commonFileListBeforeVO,
      commonFileListAfter,
      commonFileList,
      commonFileListBefore,
      __raw,
      ...baseTaskInfo
    } = taskInfo.value || {};
    const submitData = {
      ...baseTaskInfo,
      commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
      commonFileListDTO: afterModelValue.value.map(buildFileItem),
      commonFileListAfterDTO: issueModelValue.value.map(buildFileItem),
      hasException: hasException.value,
      inspectionResult: hasException.value ? 0 : 1,
      abnormalDescription: abnormalDescription.value,
      tempFileIds,
    };
    const result = await uploadInspectionTask(submitData);
    if (result && (result.code === 200 || result.success)) {
      ElMessage.success("提交成功");
      dialogVisible.value = false;
      resetState();
      emit("success");
      emit("closeDia");
    } else {
      ElMessage.error(result?.msg || result?.message || "提交失败");
    }
  } catch (error) {
    ElMessage.error(error?.message || "提交失败");
  } finally {
    loading.close();
  }
};
const removeFile = async index => {
  try {
    await ElMessageBox.confirm("确定要删除这个文件吗?", "确认删除", {
      type: "warning",
    });
    getCurrentFiles().splice(index, 1);
  } catch {}
};
const goToRepair = () => {
  const taskData = {
    taskId: taskInfo.value?.taskId || taskInfo.value?.id,
    taskName: taskInfo.value?.taskName,
    inspectionLocation: taskInfo.value?.inspectionLocation,
    inspector: taskInfo.value?.inspector,
    hasException: hasException.value,
    inspectionResult: hasException.value ? 0 : 1,
    commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
    commonFileListDTO: afterModelValue.value.map(buildFileItem),
    commonFileListAfterDTO: issueModelValue.value.map(buildFileItem),
    uploadedFiles: {
      before: beforeModelValue.value,
      after: afterModelValue.value,
      issue: issueModelValue.value,
    },
  };
  sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskData));
  router.push("/equipmentManagement/repair/add");
};
const formatFileSize = size => {
  if (!size) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let index = 0;
  let fileSize = size;
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index += 1;
  }
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
.inspection-upload-page {
  min-height: 70vh;
  background: #f5f7fa;
  padding: 20px 20px 90px;
  box-sizing: border-box;
}
.upload-content {
  max-width: 960px;
  margin: 20px auto 0;
}
.section-card {
  margin-bottom: 16px;
}
.section-card h3 {
  margin: 0 0 16px;
  font-size: 16px;
}
.upload-buttons {
  display: flex;
  gap: 12px;
  margin: 16px 0;
}
.upload-progress {
  margin-bottom: 16px;
}
.file-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
.file-preview-container {
  position: relative;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: #f2f3f5;
}
.file-preview {
  width: 100%;
  height: 100%;
}
.video-preview {
  width: 100%;
  height: 100%;
  background: #303133;
  color: #fff;
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
}
.file-info {
  margin-top: 6px;
  font-size: 12px;
}
.file-name {
  color: #606266;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size {
  color: #909399;
  margin-top: 2px;
}
.upload-summary {
  margin-top: 16px;
}
.footer-buttons {
  position: sticky;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14px 20px 0;
  background: #f5f7fa;
  display: flex;
  justify-content: center;
  gap: 12px;
}
.video-player {
  width: 100%;
  max-height: 70vh;
  background: #000;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -1,210 +1,226 @@
<template>
  <div>
    <el-dialog title="查看附件"
               v-model="dialogVisitable" width="800px" @close="cancel">
    <el-dialog title="查看附件" v-model="dialogVisitable" width="800px" @close="cancel">
      <div class="upload-container">
        <!-- ç”Ÿäº§å‰ -->
        <div class="form-container">
          <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')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          <div class="media-list">
            <img
              v-for="(item, index) in beforeProductionImgs"
              :key="`before-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(beforeProductionImgs, index, 'image')"
            />
          </div>
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
          <div class="media-list">
            <div
                v-for="(videoUrl, index) in beforeProductionVideos"
                :key="index"
                @click="showMedia(beforeProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
              v-for="(videoUrl, index) in beforeProductionVideos"
              :key="`before-video-${index}`"
              class="video-item"
              @click="showMedia(beforeProductionVideos, index, 'video')"
            >
              <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 class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
              <div class="video-text">点击播放</div>
            </div>
          </div>
        </div>
        <!-- ç”Ÿäº§åŽ -->
        <div class="form-container">
          <div class="title">生产中</div>
          <div class="media-list">
            <img
              v-for="(item, index) in afterProductionImgs"
              :key="`during-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(afterProductionImgs, index, 'image')"
            />
          </div>
          <div class="media-list">
            <div
              v-for="(videoUrl, index) in afterProductionVideos"
              :key="`during-video-${index}`"
              class="video-item"
              @click="showMedia(afterProductionVideos, index, 'video')"
            >
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </div>
              <div class="video-text">点击播放</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 class="media-list">
            <img
              v-for="(item, index) in productionIssuesImgs"
              :key="`after-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(productionIssuesImgs, index, 'image')"
            />
          </div>
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
          <div class="media-list">
            <div
                v-for="(videoUrl, index) in afterProductionVideos"
                :key="index"
                @click="showMedia(afterProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
              v-for="(videoUrl, index) in productionIssuesVideos"
              :key="`after-video-${index}`"
              class="video-item"
              @click="showMedia(productionIssuesVideos, index, 'video')"
            >
              <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 class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </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')"
                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 class="video-text">点击播放</div>
            </div>
          </div>
        </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"
            :imgs="mediaList"
            :index="currentMediaIndex"
            @hide="closeMediaViewer"
        ></vue-easy-lightbox>
        <!-- è§†é¢‘ -->
        <div v-else-if="mediaType === 'video'" style="position: relative;">
          <video
              :src="mediaList[currentMediaIndex]"
              autoplay
              controls
              style="max-width: 90vw; max-height: 80vh;"
          />
          v-if="mediaType === 'image'"
          :visible="isMediaViewerVisible"
          :imgs="mediaList"
          :index="currentMediaIndex"
          @hide="closeMediaViewer"
        />
        <div v-else-if="mediaType === 'video'" class="video-player-wrap">
          <video :src="mediaList[currentMediaIndex]" autoplay controls class="video-player" />
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import VueEasyLightbox from 'vue-easy-lightbox';
const { proxy } = getCurrentInstance();
// æŽ§åˆ¶å¼¹çª—显示
<script setup>
import { ref } from "vue";
import VueEasyLightbox from "vue-easy-lightbox";
const dialogVisitable = ref(false);
// å›¾ç‰‡æ•°ç»„
const beforeProductionImgs = ref([]);
const afterProductionImgs = ref([]);
const productionIssuesImgs = 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");
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
    }
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
const processItems = items => {
  const images = [];
  const videos = [];
  // æ£€æŸ¥ items æ˜¯å¦å­˜åœ¨ä¸”为数组
  if (!items || !Array.isArray(items)) {
  if (!Array.isArray(items)) {
    return { images, videos };
  }
  items.forEach(item => {
    if (!item || !item.previewURL || !item.contentType) return;
    if (!item) return;
    // å¤„理文件 URL
    const fileUrl = item.previewURL;
    const contentType = String(item.contentType).toLowerCase();
    const fileUrl = processFileUrl(
      item.previewURL || item.url || item.downloadUrl || item.path || ""
    );
    const contentType = String(item.contentType || "").toLowerCase();
    // æ ¹æ® contentType åˆ¤æ–­æ˜¯å›¾ç‰‡è¿˜æ˜¯è§†é¢‘
    if (contentType.startsWith('image/')) {
      images.push(fileUrl);
    } else if (contentType.startsWith('video/')) {
    if (!fileUrl) return;
    if (contentType.startsWith("video/")) {
      videos.push(fileUrl);
      return;
    }
  });
  return { images, videos };
}
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  // ä½¿ç”¨æ­£ç¡®çš„字段名:commonFileListBefore, commonFileListAfter
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBeforeVO || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfterVO || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.commonFileListVO || []);
    images.push(fileUrl);
  });
  return { images, videos };
};
const openDialog = row => {
  const { images: beforeImgs, videos: beforeVids } = processItems(
    row.commonFileListBeforeVO || []
  );
  const { images: afterImgs, videos: afterVids } = processItems(
    row.commonFileListVO || []
  );
  const { images: issueImgs, videos: issueVids } = processItems(
    row.commonFileListAfterVO || []
  );
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
  afterProductionImgs.value = afterImgs;
  afterProductionVideos.value = afterVids;
  productionIssuesImgs.value = issueImgs;
  productionIssuesVideos.value = issueVids;
  dialogVisitable.value = true;
};
// æ˜¾ç¤ºåª’体(图片 or è§†é¢‘)
function showMedia(mediaArray, index, type) {
  mediaList.value = mediaArray;
const showMedia = (items, index, type) => {
  mediaList.value = items;
  currentMediaIndex.value = index;
  mediaType.value = type;
  isMediaViewerVisible.value = true;
}
};
// å…³é—­åª’体查看器
function closeMediaViewer() {
const closeMediaViewer = () => {
  isMediaViewerVisible.value = false;
  mediaList.value = [];
  mediaType.value = 'image';
}
  mediaType.value = "image";
};
// è¡¨å•关闭方法
const cancel = () => {
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.upload-container {
  display: flex;
@@ -213,7 +229,7 @@
  padding: 20px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  .form-container {
    flex: 1;
    width: 100%;
@@ -229,7 +245,7 @@
  padding-left: 10px;
  position: relative;
  margin: 6px 0;
  &::before {
    content: "";
    position: absolute;
@@ -241,12 +257,48 @@
  }
}
.media-list {
  display: flex;
  flex-wrap: wrap;
}
.media-image {
  max-width: 100px;
  height: 100px;
  margin: 5px;
  cursor: pointer;
}
.video-item {
  position: relative;
  margin: 10px;
  cursor: pointer;
}
.video-thumb {
  width: 160px;
  height: 90px;
  background-color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
}
.video-icon {
  width: 30px;
  height: 30px;
  opacity: 0.8;
}
.video-text {
  text-align: center;
  font-size: 12px;
  color: #666;
}
.media-viewer-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
@@ -260,4 +312,13 @@
  max-height: 90vh;
  overflow: hidden;
}
</style>
.video-player-wrap {
  position: relative;
}
.video-player {
  max-width: 90vw;
  max-height: 80vh;
}
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -82,6 +82,9 @@
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <upload-files ref="uploadFiles"
                  @success="handleQuery"
                  @closeDia="handleQuery"></upload-files>
  </div>
</template>
@@ -93,6 +96,7 @@
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
  // æŽ¥å£å¼•å…¥
@@ -106,6 +110,7 @@
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
  const uploadFiles = ref();
  // æŸ¥è¯¢å‚æ•°
  const queryParams = reactive({
@@ -211,8 +216,9 @@
    const operationConfig = {
      label: "操作",
      width: 130,
      width: operations.length > 1 ? 180 : 130,
      fixed: "right",
            align: 'center',
      dataType: "action",
      operation: operations
        .map(op => {
@@ -221,6 +227,12 @@
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "upload":
              return {
                name: "上传",
                clickFun: openUploadDialog,
                color: "#409EFF",
              };
            case "viewFile":
@@ -253,14 +265,14 @@
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      const operationColumn = getOperationColumn(["upload", "viewFile"]);
      // å®šæ—¶ä»»åŠ¡è®°å½•ä¸å±•ç¤º"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
      operationsArr.value = ["upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
@@ -302,6 +314,7 @@
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
          // å¤„理 inspector å­—段
          if (processedItem.inspector) {
@@ -351,6 +364,12 @@
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
    });
  };
@@ -419,4 +438,4 @@
    color: #909399;
    font-size: 14px;
  }
</style>
</style>