gongchunyi
8 天以前 868b2427b516407e191db620f81451107525c8de
Merge branch 'dev_pro_河南鹤壁' of http://114.132.189.42:9002/r/product-inventory-management into dev_pro_河南鹤壁
已修改9个文件
1769 ■■■■ 文件已修改
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 1194 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 417 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/detail.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -85,6 +85,35 @@
            </el-form-item>
          </el-col>
        </el-row>
        <!-- 出差时间(仅当 approveType 为 3 时显示) -->
        <el-row :gutter="30" v-if="props.approveType == 3">
          <el-col :span="12">
            <el-form-item label="出差开始时间:" prop="startDateTime">
              <el-date-picker
                  v-model="form.startDateTime"
                  type="datetime"
                  placeholder="请选择开始时间"
                  value-format="YYYY-MM-DD HH:mm"
                  format="YYYY-MM-DD HH:mm"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出差结束时间:" prop="endDateTime">
              <el-date-picker
                  v-model="form.endDateTime"
                  type="datetime"
                  placeholder="请选择结束时间"
                  value-format="YYYY-MM-DD HH:mm"
                  format="YYYY-MM-DD HH:mm"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- 出差地点(仅当 approveType 为 3 时显示) -->
        <el-row v-if="props.approveType == 3">
          <el-col :span="24">
@@ -152,6 +181,8 @@
    startDate: "", // 请假开始时间
    endDate: "", // 请假结束时间
    price: null, // 报销金额
    startDateTime: "", // 出差开始时间
    endDateTime: "", // 出差结束时间
    location: "", // 出差地点
    storageBlobDTOS: []
  },
@@ -163,6 +194,8 @@
    startDate: [{ required: true, message: "请选择请假开始时间", trigger: "change" }],
    endDate: [{ required: true, message: "请选择请假结束时间", trigger: "change" }],
    price: [{ required: true, message: "请输入报销金额", trigger: "blur" }],
    startDateTime: [{ required: true, message: "请选择出差开始时间", trigger: "change" }],
    endDateTime: [{ required: true, message: "请选择出差结束时间", trigger: "change" }],
    location: [{ required: true, message: "请输入出差地点", trigger: "blur" }],
  },
});
@@ -257,8 +290,20 @@
      return
    }
  }
  // 当 approveType 为 3 时,校验出差地点
  // 当 approveType 为 3 时,校验出差时间和地点
  if (props.approveType == 3) {
    if (!form.value.startDateTime) {
      proxy.$modal.msgError("请选择出差开始时间!")
      return
    }
    if (!form.value.endDateTime) {
      proxy.$modal.msgError("请选择出差结束时间!")
      return
    }
    if (new Date(form.value.endDateTime) < new Date(form.value.startDateTime)) {
      proxy.$modal.msgError("出差结束时间不能早于开始时间!")
      return
    }
    if (!form.value.location || form.value.location.trim() === '') {
      proxy.$modal.msgError("请输入出差地点!")
      return
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -207,6 +207,7 @@
// 动态表格列配置,根据审批类型生成列
const tableColumnCopy = computed(() => {
  const isLeaveType = currentApproveType.value === 2; // 请假管理
  const isBusinessTripType = currentApproveType.value === 3; // 出差管理
  const isReimburseType = currentApproveType.value === 4; // 报销管理
  const isQuotationType = currentApproveType.value === 6; // 报价审批
  const isPurchaseType = currentApproveType.value === 5; // 采购审批
@@ -274,20 +275,32 @@
      width: 120
    });
  }
  // 请假管理:开始日期 / 结束日期
  if (isLeaveType) {
    baseColumns.push(
      { label: "开始日期", prop: "startDate", width: 120 },
      { label: "结束日期", prop: "endDate", width: 120 }
    );
  }
  
  // 日期列(根据类型动态配置)
  baseColumns.push(
    {
      label: isLeaveType ? "开始日期" : "申请日期",
      prop: isLeaveType ? "startDate" : "approveTime",
      width: 200
    },
    {
      label: "结束日期",
      prop: isLeaveType ? "endDate" : "approveOverTime",
      width: 120
    }
  );
  // 出差管理:开始时间 / 结束时间(不含秒)
  if (isBusinessTripType) {
    baseColumns.push(
      {
        label: "开始时间",
        prop: "startDateTime",
        width: 180,
        formatData: (val) => val ? val.substring(0, 16) : ''
      },
      {
        label: "结束时间",
        prop: "endDateTime",
        width: 180,
        formatData: (val) => val ? val.substring(0, 16) : ''
      }
    );
  }
  
  // 当前审批人列
  baseColumns.push({
@@ -295,6 +308,20 @@
    prop: "approveUserCurrentName",
    width: 120
  });
  // 申请时间 - 所有类型都显示
  baseColumns.push({
    label: "申请时间",
    prop: "approveTime",
    width: 180,
  });
  // 审批时间 - 所有类型都显示
  baseColumns.push({
    label: "审批时间",
    prop: "approveOverTime",
    width: 180,
  });
  
  // 操作列
  const actionOperations = [
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
@@ -1,117 +1,131 @@
<template>
  <FormDialog
    v-model="dialogVisible"
    title="上传巡检记录"
    width="980px"
    @close="handleClose"
    @cancel="handleClose"
  >
  <FormDialog v-model="dialogVisible"
              :title="operationType === 'view' ? '巡检记录详情' : '上传巡检记录'"
              width="980px"
              :operation-type="operationType"
              @close="handleClose"
              @cancel="handleClose">
    <main class="upload-content">
      <el-card v-if="taskInfo" class="section-card">
        <el-descriptions :column="1" border>
      <el-card v-if="taskInfo"
               class="section-card">
        <el-descriptions :column="2"
                         border>
          <el-descriptions-item label="巡检任务名称">
            {{ taskInfo.taskName || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="巡检项目">
            {{ taskInfo.inspectionProject || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="执行巡检人">
            <template v-if="formattedInspector && formattedInspector.length">
              <el-tag v-for="tag in formattedInspector"
                      :key="tag"
                      size="small"
                      style="margin-right: 4px">
                {{ tag }}
              </el-tag>
            </template>
            <span v-else>--</span>
          </el-descriptions-item>
          <el-descriptions-item label="频次">
            {{ formatFrequencyType(taskInfo.frequencyType) || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="开始日期与时间">
            {{ formatFrequencyDetail(taskInfo.frequencyDetail) || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="登记人">
            {{ taskInfo.registrant || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="登记日期">
            {{ formatDateTime(taskInfo.createTime) || "-" }}
          </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-group v-model="hasException"
                        :disabled="operationType === 'view'">
          <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">
      <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-input v-model="abnormalDescription"
                  type="textarea"
                  maxlength="500"
                  show-word-limit
                  :rows="4"
                  :disabled="operationType === 'view'"
                  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-card v-if="hasException === true"
               class="section-card">
        <div class="upload-buttons"
             v-if="operationType !== 'view'">
          <el-upload :show-file-list="false"
                     :http-request="uploadFile"
                     :disabled="beforeModelValue.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-upload :show-file-list="false"
                     :http-request="uploadFile"
                     :disabled="beforeModelValue.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"
          >
        <el-progress v-if="uploading"
                     :percentage="uploadProgress"
                     class="upload-progress" />
        <div v-if="beforeModelValue.length"
             class="file-list">
          <div v-for="(file, index) in beforeModelValue"
               :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>
              <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 class="delete-btn"
                         type="danger"
                         circle
                         size="small"
                         v-if="operationType !== 'view'"
                         @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" ? "图片" : "视频") }}
@@ -120,561 +134,559 @@
            </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-empty v-else
                  :description="operationType === 'view' ? '暂无异常照片或视频' : '请选择要上传的巡检图片或视频'" />
      </el-card>
      <el-result
        v-if="hasException === false"
        icon="success"
        title="设备运行正常"
        sub-title="无需上传照片"
      />
      <el-result v-if="hasException === false"
                 icon="success"
                 title="设备运行正常"
                 :sub-title="operationType === 'view' ? '' : '无需上传照片'" />
    </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 type="primary"
                   v-if="operationType !== 'view'"
                   @click="submitUpload">提交</el-button>
        <el-button v-if="hasException === true && operationType !== 'view'"
                   type="warning"
                   @click="goToRepair">
          新增报修
        </el-button>
        <el-button @click="handleClose">取消</el-button>
        <el-button @click="handleClose">{{ operationType === 'view' ? '关闭' : '取消' }}</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 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";
  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 dayjs from "dayjs";
  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 emit = defineEmits(["closeDia", "success"]);
  const router = useRouter();
const dialogVisible = ref(false);
const taskInfo = ref(null);
const uploading = ref(false);
const uploadProgress = ref(0);
  const dialogVisible = ref(false);
  const taskInfo = ref(null);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const operationType = ref("add"); // add, view
const beforeModelValue = ref([]);
const afterModelValue = ref([]);
const issueModelValue = ref([]);
  const beforeModelValue = 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}`;
  const formattedInspector = computed(() => {
    if (!taskInfo.value?.inspector) return [];
    if (Array.isArray(taskInfo.value.inspector)) return taskInfo.value.inspector;
    if (typeof taskInfo.value.inspector === "string") {
      return taskInfo.value.inspector
        .split(",")
        .map(s => s.trim())
        .filter(s => s);
    }
  }
  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)",
    return [taskInfo.value.inspector];
  });
  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 formatFrequencyType = type => {
    const mapping = {
      DAILY: "每日",
      WEEKLY: "每周",
      MONTHLY: "每月",
      QUARTERLY: "季度",
    };
    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,
    },
    return mapping[type] || type;
  };
  sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskData));
  router.push("/equipmentManagement/repair/add");
};
  const formatFrequencyDetail = detail => {
    if (typeof detail !== "string") return detail;
    const replacements = {
      MON: "周一",
      TUE: "周二",
      WED: "周三",
      THU: "周四",
      FRI: "周五",
      SAT: "周六",
      SUN: "周日",
    };
    return detail.replace(
      /MON|TUE|WED|THU|FRI|SAT|SUN/g,
      match => replacements[match]
    );
  };
const formatFileSize = size => {
  if (!size) return "0 B";
  const formatDateTime = date => {
    if (!date) return "-";
    return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
  };
  const units = ["B", "KB", "MB", "GB"];
  let index = 0;
  let fileSize = size;
  const hasException = ref(null);
  const abnormalDescription = ref("");
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index += 1;
  }
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
  const uploadConfig = {
    action: "/common/upload",
    limit: 10,
    fileSize: 50,
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
  };
defineExpose({
  openDialog,
});
  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 = [];
    hasException.value = null;
    abnormalDescription.value = "";
    uploading.value = false;
    uploadProgress.value = 0;
    showVideoDialog.value = false;
    currentVideoFile.value = null;
    operationType.value = "add";
  };
  const openDialog = (type, row) => {
    operationType.value = type || "add";
    const raw = JSON.parse(JSON.stringify(row?.__raw || row || {}));
    taskInfo.value = raw;
    beforeModelValue.value = normalizeList(
      raw.commonFileListBeforeVO || raw.commonFileListBefore || [],
      "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) {
      hasException.value = true;
    }
    dialogVisible.value = true;
  };
  const handleClose = () => {
    dialogVisible.value = false;
    resetState();
    emit("closeDia");
  };
  const previewVideo = file => {
    currentVideoFile.value = file;
    showVideoDialog.value = true;
  };
  const uploadFile = async uploadRequest => {
    const rawFile = uploadRequest.file;
    if (beforeModelValue.value.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", 10); // 生产前固定为10
    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()}`,
      };
      beforeModelValue.value.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;
      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];
      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: [],
        commonFileListAfterDTO: [],
        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",
      });
      beforeModelValue.value.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: [],
      commonFileListAfterDTO: [],
      uploadedFiles: {
        before: beforeModelValue.value,
        after: [],
        issue: [],
      },
    };
    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;
}
  .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;
}
  .upload-content {
    max-width: 960px;
    margin: 20px auto 0;
  }
.section-card {
  margin-bottom: 16px;
}
  .section-card {
    margin-bottom: 16px;
  }
.section-card h3 {
  margin: 0 0 16px;
  font-size: 16px;
}
  .section-card h3 {
    margin: 0 0 16px;
    font-size: 16px;
  }
.upload-buttons {
  display: flex;
  gap: 12px;
  margin: 16px 0;
}
  .upload-buttons {
    display: flex;
    gap: 12px;
    margin: 16px 0;
  }
.upload-progress {
  margin-bottom: 16px;
}
  .upload-progress {
    margin-bottom: 16px;
  }
.file-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
  .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-container {
    position: relative;
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: #f2f3f5;
  }
.file-preview {
  width: 100%;
  height: 100%;
}
  .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;
}
  .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;
}
  .delete-btn {
    position: absolute;
    top: 6px;
    right: 6px;
  }
.file-info {
  margin-top: 6px;
  font-size: 12px;
}
  .file-info {
    margin-top: 6px;
    font-size: 12px;
  }
.file-name {
  color: #606266;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
  .file-name {
    color: #606266;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
.file-size {
  color: #909399;
  margin-top: 2px;
}
  .file-size {
    color: #909399;
    margin-top: 2px;
  }
.upload-summary {
  margin-top: 16px;
}
  .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;
}
  .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;
}
  .video-player {
    width: 100%;
    max-height: 70vh;
    background: #000;
  }
</style>
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -1,37 +1,35 @@
<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 class="title">图片列表</div>
          <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')"
            />
            <img v-for="(item, index) in beforeProductionImgs"
                 :key="`before-img-${index}`"
                 :src="item"
                 alt=""
                 class="media-image"
                 @click="showMedia(beforeProductionImgs, index, 'image')" />
          </div>
          <div class="media-list">
            <div
              v-for="(videoUrl, index) in beforeProductionVideos"
              :key="`before-video-${index}`"
              class="video-item"
              @click="showMedia(beforeProductionVideos, index, 'video')"
            >
            <div v-for="(videoUrl, index) in beforeProductionVideos"
                 :key="`before-video-${index}`"
                 class="video-item"
                 @click="showMedia(beforeProductionVideos, index, 'video')">
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
                <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="form-container">
          <div class="title">生产中</div>
          <div class="media-list">
@@ -87,22 +85,25 @@
              <div class="video-text">点击播放</div>
            </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"
        />
        <div v-else-if="mediaType === 'video'" class="video-player-wrap">
          <video :src="mediaList[currentMediaIndex]" autoplay controls class="video-player" />
    <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" />
        <div v-else-if="mediaType === 'video'"
             class="video-player-wrap">
          <video :src="mediaList[currentMediaIndex]"
                 autoplay
                 controls
                 class="video-player" />
        </div>
      </div>
    </div>
@@ -110,215 +111,215 @@
</template>
<script setup>
import { ref } from "vue";
import VueEasyLightbox from "vue-easy-lightbox";
  import { ref } from "vue";
  import VueEasyLightbox from "vue-easy-lightbox";
const dialogVisitable = ref(false);
  const dialogVisitable = ref(false);
const beforeProductionImgs = ref([]);
const afterProductionImgs = ref([]);
const productionIssuesImgs = ref([]);
  const beforeProductionImgs = ref([]);
  const afterProductionImgs = ref([]);
  const productionIssuesImgs = ref([]);
const beforeProductionVideos = ref([]);
const afterProductionVideos = ref([]);
const productionIssuesVideos = 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");
  const isMediaViewerVisible = ref(false);
  const currentMediaIndex = ref(0);
  const mediaList = ref([]);
  const mediaType = ref("image");
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
  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}`;
    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}`;
    if (currentUrl && !currentUrl.startsWith("http")) {
      if (!currentUrl.startsWith("/")) {
        currentUrl = `/${currentUrl}`;
      }
      currentUrl = __BASE_API__ + currentUrl;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
    return currentUrl;
  };
const processItems = items => {
  const images = [];
  const videos = [];
  const processItems = items => {
    const images = [];
    const videos = [];
  if (!Array.isArray(items)) {
    if (!Array.isArray(items)) {
      return { images, videos };
    }
    items.forEach(item => {
      if (!item) return;
      const fileUrl = processFileUrl(
        item.previewURL || item.url || item.downloadUrl || item.path || ""
      );
      const contentType = String(item.contentType || "").toLowerCase();
      if (!fileUrl) return;
      if (contentType.startsWith("video/")) {
        videos.push(fileUrl);
        return;
      }
      images.push(fileUrl);
    });
    return { images, videos };
  }
  };
  items.forEach(item => {
    if (!item) return;
    const fileUrl = processFileUrl(
      item.previewURL || item.url || item.downloadUrl || item.path || ""
  const openDialog = row => {
    const { images: beforeImgs, videos: beforeVids } = processItems(
      row.commonFileListBeforeVO || []
    );
    const contentType = String(item.contentType || "").toLowerCase();
    const { images: afterImgs, videos: afterVids } = processItems(
      row.commonFileListVO || []
    );
    const { images: issueImgs, videos: issueVids } = processItems(
      row.commonFileListAfterVO || []
    );
    if (!fileUrl) return;
    beforeProductionImgs.value = beforeImgs;
    beforeProductionVideos.value = beforeVids;
    afterProductionImgs.value = afterImgs;
    afterProductionVideos.value = afterVids;
    productionIssuesImgs.value = issueImgs;
    productionIssuesVideos.value = issueVids;
    dialogVisitable.value = true;
  };
    if (contentType.startsWith("video/")) {
      videos.push(fileUrl);
      return;
    }
  const showMedia = (items, index, type) => {
    mediaList.value = items;
    currentMediaIndex.value = index;
    mediaType.value = type;
    isMediaViewerVisible.value = true;
  };
    images.push(fileUrl);
  });
  const closeMediaViewer = () => {
    isMediaViewerVisible.value = false;
    mediaList.value = [];
    mediaType.value = "image";
  };
  return { images, videos };
};
  const cancel = () => {
    dialogVisitable.value = false;
  };
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;
};
const showMedia = (items, index, type) => {
  mediaList.value = items;
  currentMediaIndex.value = index;
  mediaType.value = type;
  isMediaViewerVisible.value = true;
};
const closeMediaViewer = () => {
  isMediaViewerVisible.value = false;
  mediaList.value = [];
  mediaType.value = "image";
};
const cancel = () => {
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
  defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.upload-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  .upload-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px;
    border: 1px solid #dcdfe6;
    box-sizing: border-box;
  .form-container {
    flex: 1;
    width: 100%;
    margin-bottom: 20px;
    .form-container {
      flex: 1;
      width: 100%;
      margin-bottom: 20px;
    }
  }
}
.title {
  font-size: 14px;
  color: #165dff;
  line-height: 20px;
  font-weight: 600;
  padding-left: 10px;
  position: relative;
  margin: 6px 0;
  .title {
    font-size: 14px;
    color: #165dff;
    line-height: 20px;
    font-weight: 600;
    padding-left: 10px;
    position: relative;
    margin: 6px 0;
  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 3px;
    width: 4px;
    height: 14px;
    background-color: #165dff;
    &::before {
      content: "";
      position: absolute;
      left: 0;
      top: 3px;
      width: 4px;
      height: 14px;
      background-color: #165dff;
    }
  }
}
.media-list {
  display: flex;
  flex-wrap: wrap;
}
  .media-list {
    display: flex;
    flex-wrap: wrap;
  }
.media-image {
  max-width: 100px;
  height: 100px;
  margin: 5px;
  cursor: pointer;
}
  .media-image {
    max-width: 100px;
    height: 100px;
    margin: 5px;
    cursor: pointer;
  }
.video-item {
  position: relative;
  margin: 10px;
  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-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-icon {
    width: 30px;
    height: 30px;
    opacity: 0.8;
  }
.video-text {
  text-align: center;
  font-size: 12px;
  color: #666;
}
  .video-text {
    text-align: center;
    font-size: 12px;
    color: #666;
  }
.media-viewer-overlay {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
}
  .media-viewer-overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.8);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
  }
.media-viewer-content {
  position: relative;
  max-width: 90vw;
  max-height: 90vh;
  overflow: hidden;
}
  .media-viewer-content {
    position: relative;
    max-width: 90vw;
    max-height: 90vh;
    overflow: hidden;
  }
.video-player-wrap {
  position: relative;
}
  .video-player-wrap {
    position: relative;
  }
.video-player {
  max-width: 90vw;
  max-height: 80vh;
}
  .video-player {
    max-width: 90vw;
    max-height: 80vh;
  }
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -231,6 +231,12 @@
      operation: operations
        .map(op => {
          switch (op) {
            case "view":
              return {
                name: "详情",
                clickFun: openViewDialog,
                color: "#409EFF",
              };
            case "edit":
              return {
                name: "编辑",
@@ -273,14 +279,14 @@
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["upload", "viewFile"]);
      const operationColumn = getOperationColumn(["view", "upload", "viewFile"]);
      // 巡检记录不展示"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["upload", "viewFile"];
      operationsArr.value = ["view", "upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
@@ -377,7 +383,13 @@
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
      uploadFiles.value?.openDialog("add", row);
    });
  };
  const openViewDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog("view", row);
    });
  };
src/views/procurementManagement/procurementLedger/detail.vue
@@ -7,7 +7,7 @@
            <span class="readonly-text">{{ form.purchaseContractNumber || '--' }}</span>
          </el-form-item>
        </el-col>
        <el-col :span="12">
        <el-col v-if="showSalesContractBinding" :span="12">
          <el-form-item label="销售合同号:">
            <span class="readonly-text">{{ form.salesContractNo || '--' }}</span>
          </el-form-item>
@@ -129,6 +129,8 @@
import { getPurchaseById } from "@/api/procurementManagement/procurementLedger"
const visible = ref(false)
// 是否显示销售合同号绑定
const showSalesContractBinding = false
const form = ref({})
const productData = ref([])
const fileList = ref([])
src/views/procurementManagement/procurementLedger/index.vue
@@ -11,7 +11,7 @@
            <el-input v-model="searchForm.purchaseContractNumber" style="width: 240px" placeholder="请输入"
              @change="handleQuery" clearable :prefix-icon="Search" />
          </el-form-item>
          <el-form-item label="销售合同号:">
          <el-form-item v-if="showSalesContractBinding" label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
              @change="handleQuery" />
          </el-form-item>
@@ -76,7 +76,7 @@
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="采购合同号" prop="purchaseContractNumber" width="160" show-overflow-tooltip />
        <el-table-column label="销售合同号" prop="salesContractNo" width="160" show-overflow-tooltip />
        <el-table-column v-if="showSalesContractBinding" label="销售合同号" prop="salesContractNo" width="160" show-overflow-tooltip />
        <el-table-column label="供应商名称" prop="supplierName" width="160" show-overflow-tooltip />
        <el-table-column label="项目名称" prop="projectName" width="320" show-overflow-tooltip />
        <el-table-column label="审批状态" prop="approvalStatus" width="100" show-overflow-tooltip>
@@ -124,7 +124,7 @@
              <el-input v-model="form.purchaseContractNumber" placeholder="请输入" clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col v-if="showSalesContractBinding" :span="12">
            <el-form-item label="销售合同号:" prop="salesLedgerId">
              <el-select v-model="form.salesLedgerId" placeholder="请选择" filterable clearable @change="salesLedgerChange">
                <el-option v-for="item in salesContractList" :key="item.id" :label="item.salesContractNo" :value="item.id" />
@@ -439,6 +439,9 @@
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
const userStore = useUserStore();
// 是否显示销售合同号绑定
const showSalesContractBinding = false;
// 订单审批状态显示文本
const approvalStatusText = {
@@ -1034,7 +1037,6 @@
    if (form.value.salesLedgerId == -1) {
      form.value.salesLedgerId = null;
    }
    console.log(form.value, "form.value===========");
    dialogFormVisible.value = true;
  } catch (error) {
    console.error("打开表单失败:", error);
@@ -1536,9 +1538,7 @@
};
// 销售合同选择改变方法
const salesLedgerChange = async row => {
  console.log("row", row);
  var index = salesContractList.value.findIndex(item => item.id == row);
  console.log("index", index);
  if (index > -1) {
    await querygProductInfoByContractNo();
  }
src/views/procurementManagement/purchaseReturnOrder/New.vue
@@ -3,8 +3,10 @@
    <el-dialog
        v-model="isShow"
        title="新增采购退货"
        width="1600"
        width="70%"
        top="3vh"
        @close="closeModal"
        class="purchase-return-dialog"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef" :inline="true">
        <div class="section-title">
@@ -221,19 +223,19 @@
          <el-input style="width: 240px" v-model="formState.remark" :rows="1" type="textarea" placeholder="请输入备注"/>
        </el-form-item>
        <div style="margin: 20px 0;">
        <div style="margin:20px 0;min-width:0;">
            <div class="section-title">
              <span class="title-dot"></span>
              <span class="title-text">产品列表</span>
            </div>
            <el-button type="primary" size="small" style="margin-bottom: 20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">添加产品</el-button>
            <div class="product-table-scroll">
            <el-table class="product-table-inner"
            <el-button type="primary" size="small" style="margin-bottom:20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">添加产品</el-button>
            <el-table
                      :data="formState.purchaseReturnOrderProductsDtos"
                      border
                      max-height="400"
                      show-summary
                      :summary-method="summarizeChildrenTable">
                      :summary-method="summarizeChildrenTable"
                      style="width:100%;min-width:0;">
              <el-table-column align="center"
                               type="selection"
                               width="55" />
@@ -324,7 +326,6 @@
                </template>
              </el-table-column>
            </el-table>
            </div>
          </div>
        <div class="section-title">
@@ -803,12 +804,5 @@
  margin-right: 8px;
}
.product-table-scroll {
  width: 100%;
  overflow-x: auto;
}
.product-table-inner {
  min-width: 1280px;
}
</style>
vite.config.js
@@ -12,7 +12,7 @@
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9049"
          ? "http://1.15.17.182:9048"
          : env.VITE_JAVA_API;
  return {
    define:{