组件抽离:el-upload支持图片粘贴上传并添加预览

组件抽离:el-upload支持图片粘贴上传并添加预览 根据不同的证明材料,如:粘贴/上传图片,输入链接

【这个网站有相似功能: pqina.nl/filepond/#m...

效果图:

粘贴上传方法:

js 复制代码
<!-- 图片粘贴上传 -->

        <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>

上传的图片进行预览操作:

js 复制代码
<!-- 图片预览弹窗 -->
  <el-dialog
      v-model="imagePreviewVisible"
      title="Preview Picture"
      width="90%"
      :style="{ maxWidth: '1400px', minWidth: '800px' }"
      append-to-body
    >
      <div class="image-preview-container" ref="previewContainer">
        <div class="image-controls">
          <el-button-group>
            <el-button @click="zoomIn" :icon="ZoomIn" size="small" title="Zoom In">Zoom In</el-button>
            <el-button @click="zoomOut" size="small" :icon="ZoomOut" title="Zoom Out">Zoom Out</el-button>
            <el-button @click="resetZoom" size="small" :icon="Refresh" title="Reset">Reset</el-button>
            <el-button @click="toggleFullscreen" size="small" :icon="FullScreen" title="Fullscreen">{{ isFullscreen ? 'Exit Fullscreen' : 'Fullscreen' }}</el-button>
          </el-button-group>
          <span class="zoom-indicator">{{ Math.round(imageScale * 100) }}%</span>
        </div>
        <div 
          class="image-wrapper" 
          @wheel="handleWheel" 
          ref="imageWrapper"
        >
          <img
            :src="previewImageUrl"
            alt="预览图片"
            class="preview-image"
            :style="imageStyle"
            @error="handleImageError"
            @load="handleImageLoad"
            draggable="false"
          />
        </div>
      </div>
      <template #footer>
        <el-button @click="closePreview">Closed</el-button>
      </template>
    </el-dialog>

js方法:

js 复制代码
import { ref, computed, onMounted,onUnmounted, nextTick, reactive } from "vue";
import { Delete, Search, WarnTriangleFilled, FullScreen, ZoomIn,  ZoomOut, Refresh, Plus} from "@element-plus/icons-vue";
 import { uploadFileApiNew } from "@/api/featuredPapers/index";
 
 // 初始化加载
onMounted(async () => {
    document.addEventListener("paste", handlePaste);
    // 初始化事件监听器
    setTimeout(updateEventListeners, 500);
    // 添加拖拽区域监听
    window.addEventListener("dragover", (e) => {
      // 阻止默认行为
      e.preventDefault();
    });
    window.addEventListener("drop", handleDrop);
    document.addEventListener('fullscreenchange', () => {
        isFullscreen.value = !!document.fullscreenElement;
     });
 });

onUnmounted(() => {
  document.removeEventListener("paste", handlePaste);
  window.removeEventListener("dragover", (e) => e.preventDefault());
  window.removeEventListener("drop", handleDrop);
  document.removeEventListener('fullscreenchange', () => {
    isFullscreen.value = !!document.fullscreenElement;
  });
});

let approvalInformationObj = reactive<any>({
  performanceSettingId: "",
  commissionTypeId: "",
  jmRole: "",
  jmPaidConfirmed: "",
  jmPassRate: "",
  jmProportion: "",
  calcalationFormula: "",
  paperId: "",
  sectionId: "",
  remark: "",
  amount: "",
  siTitle: "",
  siPublished: null,
  siType: "",
  articleType: "",
  paymentAmount: "",
  requiredProofTypes: [
    {
      description:
        "邀请证明(备注:若截图部分内容较多,请提交时圈出invite,invitation, agree等类似关键词,及邮件收发时间,便于审核人员审查,提高通过率)",
      id: 3,
      type: 0,
      value:
        "Proof of Invitation(Note: If the screenshot contains extensive content, please highlight keywords like 'invite', 'invitation', or 'agree', and include the email timestamps to facilitate reviewer verification and improve approval rates.)",
      required: true,
    },
    {
      description: "学者scopus链接",
      id: 4,
      type: 1,
      value: "Scholar's Scopus Profile Link",
      required: true,
    },
    {
      description: "学者单位截图(备注:国家需体现,且与姓名在同一页展现)",
      id: 5,
      type: 0,
      value: "Screenshot of Scholar's Institutional Affiliation",
      required: true,
    },
  ],
  deTotalCommission: null,
});
// 图片上传相关
const uploadAction = ref(""); // 实际使用时替换为后端上传接口
// 新的证明材料数据结构 初始化 证明材料数据
const proofData = ref<{
  [key: number]: {
    displayUrl?: string;
    url?: string;
    note?: string;
    fileId?: number;
    images?: { url: string; name: string; fileId?: number }[];
  };
}>({
  "0": { images: [], url: "", displayUrl: "", note: "" },
  "1": { images: [], url: "", displayUrl: "", note: "" },
  "2": { images: [], url: "", displayUrl: "", note: "" },
});

// 当前焦点区域
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 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 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 customUploadProof = (options: any, index: number) => {
  handleImageFile(options.file, index);
};
// 处理图片文件
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("Image uploaded successfully.");
    } else {
      ElMessage.error("Image upload failed.");
    }
  } catch (error) {
    console.error("Upload error:", error);
    ElMessage.error("Image upload failed.");
  }
};

// 切换展示图片
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 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 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 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 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);
      }
    }
  }
};


// 图片预览相关
const imagePreviewVisible = ref(false);
const previewImageUrl = ref("");
const imageScale = ref(1);
const isFullscreen = ref(false);
const previewContainer = ref();
const imageWrapper = ref();
// 图片预览控制方法
const imageStyle = computed(() => ({
  transform: `scale(${imageScale.value})`,
  transition: 'transform 0.3s ease',
}));


// 预览图片
const previewImage = (url: string) => {

// 使用 el-dialog 预览图片
previewImageUrl.value = url;
imagePreviewVisible.value = true;
};



// 放大图片
const zoomIn = () => {
  imageScale.value = Math.min(imageScale.value * 1.2, 5);
  // 缩放后保持顶部可见
  nextTick(() => {
    if (imageWrapper.value) {
      imageWrapper.value.scrollTop = 0;
    }
  });
};

// 缩小图片
const zoomOut = () => {
  imageScale.value = Math.max(imageScale.value / 1.2, 0.1);
  // 缩放后保持顶部可见
  nextTick(() => {
    if (imageWrapper.value) {
      imageWrapper.value.scrollTop = 0;
    }
  });
};

// 重置缩放
const resetZoom = () => {
  imageScale.value = 1;
  // 重置后滚动到顶部
  nextTick(() => {
    if (imageWrapper.value) {
      imageWrapper.value.scrollTop = 0;
      imageWrapper.value.scrollLeft = 0;
    }
  });
};

// 处理鼠标滚轮缩放
const handleWheel = (event: WheelEvent) => {
  event.preventDefault();
  
  const wrapper = imageWrapper.value;
  if (!wrapper) return;
  
  // 记录当前滚动位置和鼠标相对位置
  const rect = wrapper.getBoundingClientRect();
  const mouseX = event.clientX - rect.left;
  const mouseY = event.clientY - rect.top;
  const scrollLeft = wrapper.scrollLeft;
  const scrollTop = wrapper.scrollTop;
  
  // 计算鼠标在图片中的相对位置
  const relativeX = (scrollLeft + mouseX) / imageScale.value;
  const relativeY = (scrollTop + mouseY) / imageScale.value;
  
  const oldScale = imageScale.value;
  const delta = event.deltaY > 0 ? 0.9 : 1.1;
  imageScale.value = Math.max(0.1, Math.min(5, imageScale.value * delta));
  
  // 缩放完成后调整滚动位置,保持鼠标位置不变
  nextTick(() => {
    if (wrapper) {
      const newScrollLeft = relativeX * imageScale.value - mouseX;
      const newScrollTop = relativeY * imageScale.value - mouseY;
      
      wrapper.scrollLeft = Math.max(0, newScrollLeft);
      wrapper.scrollTop = Math.max(0, newScrollTop);
    }
  });
};

// 图片加载完成时重置状态
const handleImageLoad = () => {
  resetZoom();
};

// 图片加载错误处理
const handleImageError = () => {
  ElMessage.error("图片加载失败");
};

// 全屏切换
const toggleFullscreen = () => {
  if (!document.fullscreenElement) {
    previewContainer.value?.requestFullscreen?.();
    isFullscreen.value = true;
  } else {
    document.exitFullscreen?.();
    isFullscreen.value = false;
  }
};

// 关闭预览
const closePreview = () => {
  if (isFullscreen.value) {
    document.exitFullscreen?.();
    isFullscreen.value = false;
  }
  imagePreviewVisible.value = false;
  resetZoom();
};

相关样式:

js 复制代码
.info-item {
  margin-bottom: 15px;
  display: flex;
  align-items: center;
}
.label {
  min-width: 155px;
  // font-weight: bold;
  color: #606266;
  text-align: right;
}

.two-col-container {
  display: flex;
  flex-wrap: wrap;
  gap: 16px; /* 子项之间的间距 */
  // width: 100%;
  width: 60%;
  margin-left: 10px;
}

.item {
  width: calc(50% - 8px); /* 50% 减去一半 gap 实现两列等宽 */
  border: 1px dashed #ccc;
  box-sizing: border-box;
  padding: 12px;
  // text-align: center;
  border-radius: 6px;
  background-color: #fff;
}

.proof-item {
  margin-bottom: 20px;
}
.proof-label-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.4;
  max-height: 2.8em; /* 1.4 * 2 = 2.8em for 2 lines */
  word-break: break-word;
}

.upload-container {
  margin-top: 10px;
  cursor: pointer; /* 添加指针样式,提示用户可点击 */
  padding: 5px; /* 添加内边距使点击区域更大 */
  border-radius: 6px;
  transition: background-color 0.2s;
}

.upload-container:hover {
  background-color: rgba(64, 158, 255, 0.1); /* 添加悬停效果 */
}

/* 上传区域样式优化 */
.upload-area {
  position: relative;
  border: 2px dashed transparent;
  border-radius: 8px;
  padding: 4px;
  transition: all 0.3s ease;
  cursor: pointer;

  // display: flex;
  // justify-content: center;
}

.upload-area:hover {
  border-color: #409eff;
  background-color: rgba(64, 158, 255, 0.05);
}

.upload-area.active-upload {
  border-color: #409eff;
  background-color: rgba(64, 158, 255, 0.1);
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}

.image-uploader {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  width: 148px;
  height: 148px;
  display: inline-block;
}

.image-uploader:hover {
  border-color: #409eff;
}

.uploaded-images {
  display: flex;
  flex-wrap: wrap;
  margin-top: 10px;
  gap: 8px;
  justify-content: flex-start;
  align-items: flex-start;
}
.image-item {
  position: relative;
  width: 80px;
  height: 80px;
  border: 2px solid #e4e7ed;
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  &.active {
    border: 2px solid #409eff;
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  }

  &:hover {
    border-color: #409eff;
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  }

  &:hover .image-actions {
    opacity: 1;
  }
}

.thumbnail {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.image-actions {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  transition: opacity 0.3s;
  gap: 5px;
}

.image-actions .el-button {
  color: white;
  font-size: 16px;
  padding: 8px;

  &:hover {
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 50%;
  }
}


/* 图片预览相关样式 */
.image-preview-container {
  display: flex;
  flex-direction: column;
  height: 70vh;
  user-select: none;
}

.image-controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #ebeef5;
  margin-bottom: 10px;
  flex-shrink: 0;
}

.zoom-indicator {
  font-size: 14px;
  color: #606266;
  font-weight: 500;
}

.image-wrapper {
  flex: 1;
  overflow: auto;
  position: relative;
  background-color: #f5f7fa;
  border-radius: 8px;
  /* 优化滚动性能 */
  scroll-behavior: smooth;
}

.preview-image {
  display: block;
  margin: 0 auto;
  border-radius: 8px;
  transform-origin: top center;
  user-select: none;
  /* 移除最大尺寸限制,确保图片可以正常缩放 */
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
  /* 当图片小于容器时居中显示,当图片大于容器时从顶部开始显示 */
  vertical-align: top;
}

/* 全屏状态下的样式调整 */
.image-preview-container:fullscreen {
  height: 100vh;
  background-color: #000;
  padding: 20px;
}

.image-preview-container:fullscreen .image-wrapper {
  background-color: #000;
}

.image-preview-container:fullscreen .image-controls {
  background-color: rgba(0, 0, 0, 0.8);
  color: white;
  border-radius: 8px;
  padding: 15px;
  border-bottom: 1px solid #666;
}

.image-preview-container:fullscreen .zoom-indicator {
  color: white;
}
相关推荐
快乐就好ya2 小时前
React基础到进阶
前端·javascript·react.js
小龙在山东2 小时前
VS Code 使用 Chrome DevTools MCP 实现浏览器自动化
前端·自动化·chrome devtools
悠哉摸鱼大王2 小时前
多模态场景下tts功能实现
前端·javascript
东华帝君2 小时前
__proto__对比prototype
前端
夜晓码农2 小时前
VSCode Web版本安装
前端·ide·vscode
初出茅庐的3 小时前
hooks&&状态管理&&业务管理
前端·javascript·vue.js
三掌柜6663 小时前
2025三掌柜赠书活动第三十五期 AI辅助React Web应用开发实践:基于React 19和GitHub Copilot
前端·人工智能·react.js
YH丶浩3 小时前
vue自定义数字滚动插件
开发语言·前端·javascript·vue
阿民_armin3 小时前
Canvas 冷暖色分析工具
前端·javascript·vue.js