需求:el-upload实现上传/粘贴图片功能

0 阅读9分钟

效果:

image.png

需求:

  1. 使用el-upload支持用户上传图片,粘贴图片
  2. Proof这里是上传的证明材料;原先设计的是根据不同的type,支持用户传图片或者输入相关链接

代码:

  1. 页面
<div class="info-item" style="align-items: start">
                    <span class="label">Proof:</span>
                    <div class="two-col-container">
                      <div
                        class="item"
                        v-for="(
                          proofType, index
                        ) in approvalInformationObj.requiredProofTypes"
                        :key="proofType.id"
                      >
                        <div class="proof-item">
                          <div class="label" style="text-align: left;display: flex;">
                            <span v-if="proofType.required" style="color: red"
                              >*</span
                            >
                            <span
                              class="proof-label-text"
                              :title="proofType.value"
                              >{{ proofType.value }}:</span
                            >
                          </div>
                          <div class="upload-container">
                            <!-- 图片上传类型 (type: 0) -->
                            <template v-if="proofType.type === 0">
                              <div
                                :class="`upload-area ${
                                  currentFocusArea === index
                                    ? 'active-upload'
                                    : ''
                                }`"
                                @click="setActiveUploadArea(index)"
                                @mouseenter="showUploadHint(index, $event)"
                                @mouseleave="hideUploadHint()"
                              >
                                <el-upload
                                  :class="`image-uploader proof-uploader-${index}`"
                                  :action="uploadAction"
                                  :show-file-list="false"
                                  :on-success="(response: any, file: any) => handleProofSuccess(response, file, index)"
                                  :before-upload="beforeUpload"
                                  :http-request="(options: any) => customUploadProof(options, index)"
                                  :auto-upload="true"
                                  list-type="picture-card"
                                >

                                <el-icon title="Supports uploading files"><Plus /></el-icon>

                                  <!-- <img
                                    v-if="proofData[index]?.displayUrl"
                                    :src="proofData[index].displayUrl"
                                    class="upload-image"
                                  />
                                  <div v-else class="upload-placeholder">
                                    <i class="el-icon-plus"></i>
                                    <div>Upload pictures</div>
                                  </div> -->
                                </el-upload>
                                <!-- 当前活跃上传区域指示器 -->
                                <!-- <div
                                  v-if="currentFocusArea === index"
                                  class="active-indicator"
                                >
                                  <span>当前选中</span>
                                </div> -->
                              </div>

                              <!-- 显示已上传的多个图片 -->
                              <div
                                v-if="proofData[index]?.images?.length > 0"
                                class="uploaded-images"
                              >
                                <div
                                  v-for="(image, imageIndex) in proofData[index]
                                    .images"
                                  :key="imageIndex"
                                  class="image-item"
                                  :class="{
                                    active:
                                      image.url === proofData[index].displayUrl,
                                  }"
                                >
                                  <img
                                    :src="image.url"
                                    class="thumbnail"
                                    @click="
                                      changeDisplayImage(index, image.url)
                                    "
                                  />
                                  <div class="image-actions">
                                    <el-button
                                      type="primary"
                                      :icon="Search"
                                      circle
                                      @click.stop="previewImage(image.url)"
                                      title="preview"
                                    />
                                    <el-button
                                      type="danger"
                                      :icon="Delete"
                                      circle
                                      @click.stop="
                                        removeImage(index, imageIndex)
                                      "
                                      title="delete"
                                    />
                                  </div>
                                </div>
                              </div>
                            </template>

                            <!-- 链接输入类型 (type: 1) -->
                            <template v-else-if="proofType.type === 1">
                              <el-input
                                v-model="proofData[index].url"
                                type="textarea"
                                :rows="3"
                                :placeholder="`Please input ${proofType.value} URL or text content`"
                                style="margin-top: 10px"
                              />
                            </template>
                          </div>
                          <!-- 描述信息 -->
                          <!-- <div
                            v-if="proofType.description"
                            class="proof-description"
                          >
                            {{ proofType.description }}
                          </div> -->
                        </div>
                      </div>
                    </div>
                  </div>
  1. 方法,相关方法自行搜索吧,太多了,懒得拆分了,记录下
<script lang="ts" setup>

import { formatAmount } from "@/utils/format";
import {
  CommissionType,
  CommissionTypeEnum,
  COMMISSION_TYPE_MAP,
} from "@/types/performance";

import { removeFormulaPrefix } from "@/utils/format";


const pageId = ref(0);

interface PersonWorkload {
  id: number;
  name: string;
  role: string;
  proportionNum: string;
}

interface QuarterItem {
  name: string;
  value: {
    year: number;
    beginTime: string;
    endTime: string;
    quarter: number;
  };
}

// 表单数据
const commissionType = ref<CommissionType>("");
const formulaBase = ref(36);
const formulaMultiplier = ref(180);
const selectedCategory = ref();
const selectedQuarter = ref();

// Other类型相关变量
const selectedJournal = ref();

const selectedRole = ref();
const proportionNum = ref();

const commissionTypeEnumList = ref<CommissionTypeEnum[]>([]);
const categoryList = ref([]);

// 计算当前选中category的label值,用于title属性
const selectedCategoryLabel = computed(() => {
  if (!selectedCategory.value || !categoryList.value.length) {
    return "";
  }
  const selectedItem = categoryList.value.find(
    (item: any) => item.id === selectedCategory.value
  );
  return selectedItem ? selectedItem.value : "";
});

let personalInformationObj = reactive<any>({
  email: "",
  chineseName: "",
  role: "",
  journals: [],
  time: "",
  qcPassRate: "",
  paidConfirmed: "",
  pi: null,
  createdTime: "", 
  finalConfirmed: ''
});
interface ProofType {
  id: number;
  type: number;
  value: string;
  description: string;
  required: boolean;
}

let approvalInformationObj = reactive({
  performanceSettingId: "",
  commissionTypeId: "",
  jmRole: "",
  jmPaidConfirmed: "",
  jmPassRate: "",
  jmProportion: "",
  calcalationFormula: "",
  paperId: "",
  sectionId: "",
  remark: "",
  amount: "",
  siTitle: "",
  siPublished: null,
  siType: "",
  articleType: "",
  paymentAmount: "",
  requiredProofTypes: [] as ProofType[],
  deTotalCommission: null,
});

const showSave = ref(false);
const showSendToApprove = ref(false);

// 图片上传相关
const uploadAction = ref(""); // 实际使用时替换为后端上传接口

// 新的证明材料数据结构
const proofData = ref<{
  [key: number]: {
    displayUrl?: string;
    url?: string;
    note?: string;
    fileId?: number;
    images?: { url: string; name: string; fileId?: number }[];
  };
}>({});

// 压缩包上传相关数据
const uploadedFiles = ref<{
  name: string;
  size: number;
  fileId?: number;
  originalFile?: File;
}[]>([]);;

// 当前焦点区域
const currentFocusArea = ref<any>(0);
// 是否显示焦点提示
const showFocusHint = ref(false);
// 焦点提示的位置
const focusHintPosition = ref({ top: 0, left: 0 });

// 设置活跃上传区域
const setActiveUploadArea = (index: number) => {
  currentFocusArea.value = index;

  // 显示简短提示
  showFocusHint.value = true;
  setTimeout(() => {
    showFocusHint.value = false;
  }, 1500);
};

// 显示上传提示
const showUploadHint = (index: number, event: MouseEvent) => {
  const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
  focusHintPosition.value = {
    top: rect.top - 30,
    left: rect.left + rect.width / 2 - 100,
  };

  // 临时更新焦点区域用于显示提示文本
  const tempFocus = currentFocusArea.value;
  currentFocusArea.value = index;
  showFocusHint.value = true;

  // 恢复原来的焦点区域
  setTimeout(() => {
    if (currentFocusArea.value === index && !showFocusHint.value) {
      currentFocusArea.value = tempFocus;
    }
  }, 100);
};

// 隐藏上传提示
const hideUploadHint = () => {
  showFocusHint.value = false;
};

// 显示焦点提示
const showFocusIndicator = (e: MouseEvent, index: number) => {
  // 更新当前焦点区域
  currentFocusArea.value = index;

  // 计算提示位置
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
  focusHintPosition.value = {
    top: rect.top - 25,
    left: rect.left + rect.width / 2 - 75,
  };

  // 显示提示
  showFocusHint.value = true;

  // 3秒后自动隐藏
  setTimeout(() => {
    showFocusHint.value = false;
  }, 2000);
};

// 更新事件监听器
const updateEventListeners = () => {
  // 移除旧的事件监听器,避免重复绑定
  document.querySelectorAll(".upload-container").forEach((container) => {
    container.removeEventListener("click", () => {});
  });

  // 添加新的事件监听器
  const invitationUploader = document.querySelector(".invitation-uploader");
  const scopusUploader = document.querySelector(".scopus-uploader");
  const referencesUploader = document.querySelector(".references-uploader");
  const identityUploader = document.querySelector(".identity-uploader");

  if (invitationUploader) {
    invitationUploader.addEventListener("click", (e) => {
      currentFocusArea.value = "invitation";
      showFocusIndicator(e as MouseEvent, "invitation");
    });
  }

  if (scopusUploader) {
    scopusUploader.addEventListener("click", (e) => {
      currentFocusArea.value = "scopus";
      showFocusIndicator(e as MouseEvent, "scopus");
    });
  }

  if (referencesUploader) {
    referencesUploader.addEventListener("click", (e) => {
      currentFocusArea.value = "references";
      showFocusIndicator(e as MouseEvent, "references");
    });
  }

  if (identityUploader) {
    identityUploader.addEventListener("click", (e) => {
      currentFocusArea.value = "identity";
      showFocusIndicator(e as MouseEvent, "identity");
    });
  }

  // 添加上传区域的容器监听
  document.querySelectorAll(".upload-container").forEach((container) => {
    // 判断这是哪个上传区域
    if (container.querySelector(".invitation-uploader")) {
      container.addEventListener("click", (e) => {
        if (e.target === container) {
          currentFocusArea.value = "invitation";
          showFocusIndicator(e as MouseEvent, "invitation");
          // 点击容器时,模拟点击上传组件
          const uploader = container.querySelector(".invitation-uploader");
          if (uploader && uploader instanceof HTMLElement) {
            uploader.click();
          }
        }
      });
    } else if (container.querySelector(".scopus-uploader")) {
      container.addEventListener("click", (e) => {
        if (e.target === container) {
          currentFocusArea.value = "scopus";
          showFocusIndicator(e as MouseEvent, "scopus");
          // 点击容器时,模拟点击上传组件
          const uploader = container.querySelector(".scopus-uploader");
          if (uploader && uploader instanceof HTMLElement) {
            uploader.click();
          }
        }
      });
    } else if (container.querySelector(".references-uploader")) {
      container.addEventListener("click", (e) => {
        if (e.target === container) {
          currentFocusArea.value = "references";
          showFocusIndicator(e as MouseEvent, "references");
          // 点击容器时,模拟点击上传组件
          const uploader = container.querySelector(".references-uploader");
          if (uploader && uploader instanceof HTMLElement) {
            uploader.click();
          }
        }
      });
    } else if (container.querySelector(".identity-uploader")) {
      container.addEventListener("click", (e) => {
        if (e.target === container) {
          currentFocusArea.value = "identity";
          showFocusIndicator(e as MouseEvent, "identity");
          // 点击容器时,模拟点击上传组件
          const uploader = container.querySelector(".identity-uploader");
          if (uploader && uploader instanceof HTMLElement) {
            uploader.click();
          }
        }
      });
    }
  });
};

const quarterList = ref<QuarterItem[]>([]);

// 获取当前选中季度的数据
const currentQuarterData = computed(() => {
  return quarterList.value?.find((item) => item.name === selectedQuarter.value);
});

const needTimeTypes = [
  CommissionType.DE_JOB_GRADE,
  CommissionType.AE_JOB_GRADE,
];

const beginTime = !needTimeTypes.includes(commissionType.value)
  ? currentQuarterData.value?.value.beginTime
  : null;
const endTime = !needTimeTypes.includes(commissionType.value)
  ? currentQuarterData.value?.value.endTime
  : null;



// 存储原始金额,用于按比例计算
const originalAmount = ref<string>("");

// 计算金额的函数
const calculateAmount = () => {
  if (proportionNum.value && originalAmount.value) {
    const proportion = parseFloat(proportionNum.value) / 100; // 将百分比转换为小数
    const original = parseFloat(originalAmount.value);
    if (!isNaN(proportion) && !isNaN(original)) {
      approvalInformationObj.amount = (original * proportion).toFixed(2);
    }
  } else if (!proportionNum.value && originalAmount.value) {
    // 如果比例为空,恢复原始金额
    approvalInformationObj.amount = originalAmount.value;
  }
};

// 监听proportionNum变化,重新计算amount
watch(proportionNum, () => {
  calculateAmount();
});

const canEditRequestedAmount = ref(false);
const performanceSettingLength = ref();

const sameApplications = ref(false)

// 查询申请信息,及基本信息qc pass rate, paid confirmed, pi等信息
const getApprovalDetailsData = (
  categoryId: number | string,
  beginTime?: string,
  endTime?: string,
  needCategoryList: boolean = true,
  uncheckedNumber: number = 0
) => {

  const needPaperId = [CommissionType.PAPER_EXTRA, CommissionType.OTHER].includes(commissionType.value);
  const needSectionId = [CommissionType.SI_EXTRA, CommissionType.OTHER].includes(commissionType.value);


  performanceSettingsByCommissionId({
    commissionId: commissionType.value,
    categoryId: categoryId || 0,
    journalId: selectedJournal.value,
    beginTime: beginTime || null,
    endTime: endTime || null,
    requestedId: needPaperId ? approvalInformationObj.paperId : null, // 文章id
    sectionId: needSectionId ? approvalInformationObj.sectionId : null,
    uncheckedNumber: uncheckedNumber || uncheckedIds.value.length
  }).then((res) => {
    if (res && res.code === 2000 && res.data) {
      // 获取category数据
      if (needCategoryList) {
        categoryList.value = res.data.performanceCategoryEnumList || [];
      }
      // 获取基本信息
      personalInformationObj.qcPassRate =
        res.data.calculateConfigValueMap?.qcPassRate || 0;
      personalInformationObj.paidConfirmed =
        res.data.calculateConfigValueMap?.paidConfirmedNumber || 0;

      personalInformationObj.finalConfirmed = personalInformationObj.paidConfirmed - uncheckedIds.value.length


      personalInformationObj.pi =
        res.data.calculateConfigValueMap?.piNumber || 0;

      // 获取申请信息
      approvalInformationObj.calcalationFormula =
        res.data.performanceSetting?.[0]?.amount || "";

      // 保存原始金额
      const requestedAmount =
        res.data.performanceSetting?.[0]?.requestedAmount ?? "";
      originalAmount.value = requestedAmount;
      approvalInformationObj.amount = requestedAmount;

      // 如果proportionNum已有值,重新计算金额
      setTimeout(() => {
        calculateAmount();
      }, 0);

      approvalInformationObj.performanceSettingId =
        res.data.performanceSetting?.[0]?.id || "";
      approvalInformationObj.commissionTypeId =
        res.data.performanceSetting?.[0]?.commissionTypeId || "";

     
      approvalInformationObj.articleType = res.data.article?.articleType;
      approvalInformationObj.paymentAmount = res.data.article?.apc;

      approvalInformationObj.requiredProofTypes =
        res.data.requiredProofTypes || [];
      approvalInformationObj.deTotalCommission =
        res.data.calculateConfigValueMap?.deTotalCommission || 0;

      canEditRequestedAmount.value = res.data.canEditRequestedAmount;
      performanceSettingLength.value = res.data.performanceSetting?.length;
      sameApplications.value = res.data.sameApplications?.length > 0

      // 初始化 证明材料数据
      if (
        res.data.requiredProofTypes &&
        res.data.requiredProofTypes.length > 0
      ) {
        res.data.requiredProofTypes.forEach((_, index: number) => {
          if (!proofData.value[index]) {
            proofData.value[index] = {
              images: [],
              url: "",
              displayUrl: "",
              note: "",
              fileId: undefined,
            };
          }
        });
      }
    }
  });
};

const journalOptions = ref<any[]>([]);
const roleOptions = ref<any[]>([]);

// 监听粘贴事件,实现粘贴上传图片
onMounted(() => {
  document.addEventListener("paste", handlePaste);

  // 初始化事件监听器
  setTimeout(updateEventListeners, 500);

  // 添加拖拽区域监听
  window.addEventListener("dragover", (e) => {
    // 阻止默认行为
    e.preventDefault();
  });

  window.addEventListener("drop", handleDrop);

 


  return () => {
    window.removeEventListener("dragover", (e) => e.preventDefault());
  };
});



// 处理拖拽事件
const handleDrop = (e: DragEvent) => {
  e.preventDefault();
  // 检查拖拽的是否为文件
  if (e.dataTransfer?.files.length) {
    // 根据拖拽位置判断目标区域
    const target = document.elementFromPoint(e.clientX, e.clientY);
    if (!target) return;

    // 查找最近的上传区域
    let proofIndex = -1;
    let container = null;

    // 尝试从点击元素向上查找上传容器
    let element = target as HTMLElement;
    while (element && proofIndex === -1) {
      if (element.classList?.contains("upload-container")) {
        container = element;
        // 查找proof-uploader的索引
        const uploader = element.querySelector("[class*='proof-uploader-']");
        if (uploader) {
          const classList = Array.from(uploader.classList);
          const uploaderClass = classList.find((cls) =>
            cls.startsWith("proof-uploader-")
          );
          if (uploaderClass) {
            proofIndex = parseInt(uploaderClass.split("-")[2]);
          }
        }
        break;
      }

      // 直接检查当前元素是否是上传器
      if (element.classList.value.includes("proof-uploader-")) {
        const classList = Array.from(element.classList);
        const uploaderClass = classList.find((cls) =>
          cls.startsWith("proof-uploader-")
        );
        if (uploaderClass) {
          proofIndex = parseInt(uploaderClass.split("-")[2]);
        }
        break;
      }

      element = element.parentElement as HTMLElement;
    }

    if (proofIndex !== -1) {
      // 更新当前焦点区域
      currentFocusArea.value = proofIndex;
      // 处理所有拖拽的文件
      Array.from(e.dataTransfer.files).forEach((file) => {
        if (file.type.startsWith("image/")) {
          handleImageFile(file, proofIndex);
        }
      });

      // 如果找到了容器,显示焦点提示
      if (container) {
        showFocusIndicator(e as unknown as MouseEvent, proofIndex);
      }
    }
  }
};

// 组件卸载时移除事件监听
onUnmounted(() => {
  document.removeEventListener("paste", handlePaste);
  window.removeEventListener("dragover", (e) => e.preventDefault());
  window.removeEventListener("drop", handleDrop);
});

// 处理粘贴事件
const handlePaste = (event: ClipboardEvent) => {
  const items = event.clipboardData?.items;
  if (!items) return;

  // 检查当前焦点元素,判断应该上传到哪个区域
  const activeElement = document.activeElement;
  let targetIndex = currentFocusArea.value;
  let foundArea = false;

  // 根据当前激活的元素判断上传区域
  if (activeElement) {
    // 尝试从当前元素向上查找上传容器
    let element = activeElement as HTMLElement;
    while (element && !foundArea) {
      if (element.classList?.contains("upload-container")) {
        // 查找proof-uploader的索引
        const uploader = element.querySelector("[class*='proof-uploader-']");
        if (uploader) {
          const classList = Array.from(uploader.classList);
          const uploaderClass = classList.find((cls) =>
            cls.startsWith("proof-uploader-")
          );
          if (uploaderClass) {
            targetIndex = parseInt(uploaderClass.split("-")[2]);
            foundArea = true;
          }
        }
        break;
      }

      // 直接检查当前元素是否是上传器
      if (element.classList.value.includes("proof-uploader-")) {
        const classList = Array.from(element.classList);
        const uploaderClass = classList.find((cls) =>
          cls.startsWith("proof-uploader-")
        );
        if (uploaderClass) {
          targetIndex = parseInt(uploaderClass.split("-")[2]);
          foundArea = true;
        }
        break;
      }

      element = element.parentElement as HTMLElement;
    }
  }

  let imageFound = false;

  // 处理所有粘贴的图片
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") !== -1) {
      const file = items[i].getAsFile();
      if (!file) continue;

      imageFound = true;
      // 使用确定的目标区域上传图片
      handleImageFile(file, targetIndex);
    }
  }

  // 只有找到图片时才阻止默认粘贴行为
  if (imageFound) {
    event.preventDefault();
    // 更新当前焦点区域,以便下次粘贴时使用
    currentFocusArea.value = targetIndex;
  }
};

// 上传前检查
const beforeUpload = (file: File) => {
  const isImage = file.type.startsWith("image/");
  const isLt5M = file.size / 1024 / 1024 < 5;

  if (!isImage) {
    ElMessage.error("Only image files can be uploaded!");
    return false;
  }
  if (!isLt5M) {
    ElMessage.error("The image size must not exceed 5MB.");
    return false;
  }
  return true;
};

// 处理图片文件
const handleImageFile = async (file: File, index: number) => {
  if (!beforeUpload(file)) return;

  // 初始化该索引的数据对象
  if (!proofData.value[index]) {
    proofData.value[index] = { images: [] };
  }
  if (!proofData.value[index].images) {
    proofData.value[index].images = [];
  }

  try {
    // 调用真实上传接口
    const uploadResponse = await uploadFileApiNew(file);

    if (uploadResponse.code === 2000 && uploadResponse.data) {
      const fileId = uploadResponse.data.uploadFileData.id || "";
      const imgUrl = URL.createObjectURL(file);
      const imageData = {
        url: imgUrl,
        name: file.name,
        fileId: fileId,
      };

      proofData.value[index].images!.push(imageData);
      proofData.value[index].displayUrl = imgUrl;
      proofData.value[index].fileId = fileId; // 记录当前显示图片的文件ID

      ElMessage.success("mage uploaded successfully.");
    } else {
      ElMessage.error("Image upload failed.");
    }
  } catch (error) {
    console.error("Upload error:", error);
    ElMessage.error("Image upload failed.");
  }
};

// 自定义上传函数
const customUploadProof = (options: any, index: number) => {
  handleImageFile(options.file, index);
};

// 上传成功回调
const handleProofSuccess = async (response: any, file: any, index: number) => {
  // 初始化该索引的数据对象
  if (!proofData.value[index]) {
    proofData.value[index] = { images: [] };
  }
  if (!proofData.value[index].images) {
    proofData.value[index].images = [];
  }

  try {
    // 调用真实上传接口
    const uploadResponse = await uploadFileApiNew(file.raw);

    if (uploadResponse.code === 2000 && uploadResponse.data) {
      const fileId = uploadResponse.data.id;
      const url = URL.createObjectURL(file.raw);

      proofData.value[index].images!.push({
        url,
        name: file.name,
        fileId: fileId,
      });
      proofData.value[index].displayUrl = url;
      proofData.value[index].fileId = fileId; // 记录当前显示图片的文件ID

      ElMessage.success("Image uploaded successfully.");
    } else {
      ElMessage.error("Image upload failed.");
    }
  } catch (error) {
    console.error("Upload error:", error);
    ElMessage.error("Image upload failed.");
  }
};

// 移除图片
const removeImage = (proofIndex: number, imageIndex: number) => {
  if (proofData.value[proofIndex]?.images) {
    proofData.value[proofIndex].images!.splice(imageIndex, 1);

    if (proofData.value[proofIndex].images!.length === 0) {
      proofData.value[proofIndex].displayUrl = "";
    } else {
      proofData.value[proofIndex].displayUrl =
        proofData.value[proofIndex].images![0].url;
    }
  }
};

// 预览图片
const previewImage = (url: string) => {
  // 使用 el-dialog 预览图片
  previewImageUrl.value = url;
  imagePreviewVisible.value = true;
};

// 切换展示图片
const changeDisplayImage = (proofIndex: number, url: string) => {
  if (proofData.value[proofIndex]) {
    proofData.value[proofIndex].displayUrl = url;

    // 找到对应图片的文件ID并更新
    const selectedImage = proofData.value[proofIndex].images?.find(
      (img) => img.url === url
    );
    if (selectedImage?.fileId) {
      proofData.value[proofIndex].fileId = selectedImage.fileId;
    }
  }
};

// 验证证明材料
const validateProofMaterials = () => {
  for (let i = 0; i < approvalInformationObj.requiredProofTypes.length; i++) {
    const proofType = approvalInformationObj.requiredProofTypes[i];

    if (proofType.required) {
      if (proofType.type === 0) {
        // 图片上传类型检查
        if (!proofData.value[i]?.images?.length) {
          ElMessage.warning(`Please upload the supporting document for ${proofType.value}`);
          return false;
        }
      } else if (proofType.type === 1) {
        // 链接输入类型检查
        if (!proofData.value[i]?.url?.trim()) {
          ElMessage.warning(`Please enter the content for ${proofType.value}.`);
          return false;
        }
      }
    }
  }
  return true;
};

const applicationId = ref();

// 存储未勾选记录的ID列表
const uncheckedIds = ref<string[]>([]);

// 处理未勾选ID列表更新
const handleUncheckedIdsUpdate = (ids: string[]) => {
  uncheckedIds.value = ids;
};

// 处理弹窗关闭事件
const handleDialogClosed = (uncheckedNumber: number) => {
  // 弹窗关闭时调用接口,传递uncheckedNumber参数
  if (currentQuarterData.value) {
    getApprovalDetailsData(
      selectedCategory.value || 0,
      currentQuarterData.value.value.beginTime,
      currentQuarterData.value.value.endTime,
      false, // needCategoryList
      uncheckedNumber // 传递未勾选数量
    );
  }
};

const handleSendToApproval = async () => {
  // 验证证明材料
  if (!validateProofMaterials()) {
    return;
  }

};



const handleSave = () => {
  if (!commissionType.value) {
    return ElMessage.warning("Commission Type cannot be empty.");
  }

  // 验证Time必填(当Time字段显示时)
  const needTimeTypes = [
    CommissionType.DE_JOB_GRADE,
    CommissionType.AE_JOB_GRADE,
  ];
  if (!needTimeTypes.includes(commissionType.value) && !selectedQuarter.value) {
    return ElMessage.warning("Time cannot be empty.");
  }

  const needCategoryTypes = [
    CommissionType.PAPER_EXTRA,
    CommissionType.SI_EXTRA,
    CommissionType.OTHER,
    CommissionType.DE_JOB_GRADE,
    CommissionType.AE_JOB_GRADE,
  ];
  if (
    needCategoryTypes.includes(commissionType.value) &&
    !selectedCategory.value
  ) {
    return ElMessage.warning("Category cannot be empty.");
  }

  // 验证paper id
  if (
    [CommissionType.PAPER_EXTRA].includes(commissionType.value) &&
    !approvalInformationObj.paperId
  ) {
    return ElMessage.warning("Paper ID cannot be empty.");
  }

  // 验证SI ID必填(当SI ID字段显示时)
  const needSectionIdTypes = [CommissionType.SI_EXTRA];
  if (
    needSectionIdTypes.includes(commissionType.value) &&
    !approvalInformationObj.sectionId?.trim()
  ) {
    return ElMessage.warning("SI ID cannot be empty.");
  }

  // 验证Remark必填
  if (!approvalInformationObj.remark?.trim()) {
    return ElMessage.warning("Remark cannot be empty.");
  }

   // 验证证明材料
   if (!validateProofMaterials()) {
    return;
  }

  if(sameApplications.value && !approvalInformationObj.paperId){
    return ElMessage.warning("Please note that this ID has already been submitted. ");
  }
  if(sameApplications.value && !approvalInformationObj.sectionId){
    return ElMessage.warning("Please note that this ID has already been submitted. ");
  }
  

  // 构建证明材料数据 - 按照后端要求的格式
  const proofRelations = approvalInformationObj.requiredProofTypes
    .map((proofType, index) => {
      const data = proofData.value[index];

      if (proofType.type === 0) {
        // 图片类型:如果有多张图片,每张图片作为一条数据
        if (data?.images?.length > 0) {
          return data.images.map((image) => ({
            fileId: image.fileId || null,
            note: "",
            fileType: proofType.id,
          }));
        } else {
          // 没有图片时返回一条空数据
          // return [
          //   {
          //     fileId: null,
          //     note: "",
          //     fileType: proofType.id,
          //   },
          // ];
        }
      } else if (proofType.type === 1) {
        // 文本输入类型:只返回用户输入的文本,fileId为空
        return [
          {
            fileId: null,
            note: data?.url || "",
            fileType: proofType.id,
          },
        ];
      }

      return null;
    })
    .filter((item) => item !== null) // 过滤掉空值
    .flat(); // 将嵌套数组展平

  // 添加文件上传数据 (当Commission Type为Other且Category为26时)
  const fileUploadRelations: any = [];
  if (commissionType.value === CommissionType.OTHER && selectedCategory.value === 26 && uploadedFiles.value.length > 0) {
   
    uploadedFiles.value.forEach((file) => {
      const fileRelation = {
        fileId: file.fileId || null,
        note: "",
        fileType: -1, // 文件类型固定传-1
      };
      fileUploadRelations.push(fileRelation);
    });
  }

  // 合并证明材料和文件上传数据
  const allProofRelations = [...proofRelations, ...fileUploadRelations];

  const needPaperId = [CommissionType.PAPER_EXTRA, CommissionType.OTHER].includes(commissionType.value);
  const needSectionId = [CommissionType.SI_EXTRA, CommissionType.OTHER].includes(commissionType.value);


  ElMessageBox.confirm("Confirm the approval of this application?", "Tips", {
    confirmButtonText: "Confirm",
    cancelButtonText: "Cancel",
    type: "success",
  })
    .then(() => {
      performanceAppCreate(params).then((res: any) => {
        if (res.code === 2000) {
          ElMessage({
            type: "success",
            message: res.message,
          });

          applicationId.value = res.data.application?.id;

          let routeData = router.resolve({
            path: needTimeTypes.includes(commissionType.value)
              ? "/performanceManagement/jobGradeApprovals"
              : "/performanceManagement/approvalRequestList",
            query: {
              approverEmail: personalInformationObj.email,
            },
          });
          window.open(routeData.href, "_blank");
        }
      });
    })
    .catch(() => {});
};



const rulesDialogVisible = ref(false);
const performanceSettingData = ref();

// 图片预览相关
const imagePreviewVisible = ref(false);
const previewImageUrl = ref("");

// 添加显示Matching Rules详情的方法
const showMatchingRulesDetails = () => {
  tableDialogTitle.value = "Matching Rules Details";
  rulesDialogVisible.value = true;

  performanceSettingsRuleId({
    id: approvalInformationObj.performanceSettingId,
  }).then((res) => {
    if (res.code === 2000) {
      performanceSettingData.value = res.data.performanceSetting;
    }
  });
};




const cancelModal = () => {
  rulesDialogVisible.value = false
}

// 文件上传相关方法
const beforeFileUpload = (file: File) => {
  const validExtensions = [
    '.zip', '.rar', '.7z', '.tar', '.gz',  // 压缩包
    '.pdf',                                // PDF文档
    '.doc', '.docx',                       // Word文档
    '.xls', '.xlsx',                       // Excel文档
    '.ppt', '.pptx',                       // PowerPoint文档
    '.txt', '.csv',                        // 文本文件
    '.json', '.xml'                        // 数据文件
  ];
  const fileName = file.name.toLowerCase();
  const isValidType = validExtensions.some(ext => fileName.endsWith(ext));
  const isLt50M = file.size / 1024 / 1024 < 50;

  if (!isValidType) {
    ElMessage.error("Supported file types: .zip, .rar, .7z, .tar, .gz, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .csv, .json, .xml");
    return false;
  }
  if (!isLt50M) {
    ElMessage.error("The file size must not exceed 50MB.");
    return false;
  }
  return true;
};

// 自定义文件上传函数
const customFileUpload = async (options: any) => {
  const file = options.file;
  
  if (!beforeFileUpload(file)) return;
  
  try {
    const uploadResponse = await uploadFileApiNew(file);
    
    if (uploadResponse.data) {
      // 根据API响应结构获取正确的fileId
      const fileId = uploadResponse.data.uploadFileData?.id || uploadResponse.data.id;
      
      const fileData = {
        name: file.name,
        size: file.size,
        fileId: fileId,
        originalFile: file
      };
      
      uploadedFiles.value.push(fileData);
      console.log("文件上传成功,fileId:", fileId, "文件数据:", fileData);
      ElMessage.success("File uploaded successfully.");
    } else {
      ElMessage.error("File upload failed.");
    }
  } catch (error) {
    console.error("Upload error:", error);
    ElMessage.error("File upload failed.");
  }
};

// 处理文件上传成功
const handleFileUploadSuccess = (response: any, file: any) => {
  // 这个方法在使用 http-request 时不会被调用,但保留以防需要
};

// 移除已上传的文件
const removeUploadedFile = (index: number) => {
  uploadedFiles.value.splice(index, 1);
  ElMessage.success("File removed successfully.");
};

// 格式化文件大小
const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
</script>
  1. 测试数据
 "requiredProofTypes": [
            {
                "description": "222222",
                "id": 3,
                "type": 0,
                "value": "222222",
                "required": true  // 判断是否必填
            },
            {
                "description": "链接",
                "id": 4,
                "type": 1,
                "value": "Link",
                "required": true
            },
            {
                "description": "hhhhh",
                "id": 5,
                "type": 0,
                "value": "hhhhh",
                "required": true
            }
        ],