WXL
2026-08-12 5b26f73b191ca95f12df24ec3e3d7930b4590be7
src/components/CustomImageViewer/index.vue
@@ -1,14 +1,34 @@
<template>
  <div v-if="visible" class="custom-image-viewer">
  <div v-if="visible" class="custom-image-viewer" @wheel.prevent="handleWheel">
    <!-- 遮罩层 -->
    <div class="el-image-viewer__mask" @click.self="handleMaskClick"></div>
    <!-- 图片预览主体 -->
    <!-- 关闭 -->
    <div class="el-image-viewer__btn el-image-viewer__close" @click="close">
      <i class="el-icon-close"></i>
    </div>
    <!-- 上一张按钮 -->
    <!-- 工具栏 -->
    <div class="viewer-toolbar">
      <div class="toolbar-btn" title="放大" @click="zoomIn">
        <i class="el-icon-zoom-in"></i>
      </div>
      <div class="toolbar-btn" title="缩小" @click="zoomOut">
        <i class="el-icon-zoom-out"></i>
      </div>
      <div class="toolbar-btn" title="顺时针旋转" @click="rotateRight">
        <i class="el-icon-refresh-right"></i>
      </div>
      <div class="toolbar-btn" title="逆时针旋转" @click="rotateLeft">
        <i class="el-icon-refresh-left"></i>
      </div>
      <div class="toolbar-btn" title="还原" @click="resetTransform">
        <i class="el-icon-s-home"></i>
      </div>
      <span class="toolbar-info">{{ currentIndex + 1 }} / {{ urlList.length }}</span>
    </div>
    <!-- 上一张 -->
    <div
      class="el-image-viewer__btn el-image-viewer__prev"
      :class="{ 'is-disabled': isFirst }"
@@ -17,7 +37,7 @@
      <i class="el-icon-arrow-left"></i>
    </div>
    <!-- 下一张按钮 -->
    <!-- 下一张 -->
    <div
      class="el-image-viewer__btn el-image-viewer__next"
      :class="{ 'is-disabled': isLast }"
@@ -27,17 +47,25 @@
    </div>
    <!-- 图片展示 -->
    <div class="el-image-viewer__canvas">
    <div
      class="el-image-viewer__canvas"
      @mousedown="handleMouseDown"
      @mousemove="handleMouseMove"
      @mouseup="handleMouseUp"
      @mouseleave="handleMouseUp"
    >
      <img
        v-if="currentUrl"
        :src="currentUrl"
        :key="currentIndex"
        @error="handleError"
        class="el-image-viewer__img"
        :style="imgStyle"
        draggable="false"
      />
    </div>
    <!-- 自定义提示信息 -->
    <!-- 提示信息 -->
    <div v-if="showTip" class="custom-viewer-tip">
      {{ tipMessage }}
    </div>
@@ -48,24 +76,22 @@
export default {
  name: 'CustomImageViewer',
  props: {
    urlList: {
      type: Array,
      default: () => []
    },
    initialIndex: {
      type: Number,
      default: 0
    },
    visible: {
      type: Boolean,
      default: false
    }
    urlList: { type: Array, default: () => [] },
    initialIndex: { type: Number, default: 0 },
    visible: { type: Boolean, default: false }
  },
  data() {
    return {
      currentIndex: this.initialIndex,
      showTip: false,
      tipMessage: ''
      tipMessage: '',
      scale: 1,
      rotate: 0,
      // 拖拽
      dragging: false,
      dragStart: { x: 0, y: 0 },
      offsetX: 0,
      offsetY: 0,
    };
  },
  computed: {
@@ -77,174 +103,159 @@
    },
    isLast() {
      return this.currentIndex === this.urlList.length - 1;
    }
    },
    imgStyle() {
      return {
        transform: `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale}) rotate(${this.rotate}deg)`,
        transition: this.dragging ? 'none' : 'transform 0.3s',
        cursor: this.scale > 1 ? (this.dragging ? 'grabbing' : 'grab') : 'default',
      };
    },
  },
  watch: {
    initialIndex(newVal) {
      this.currentIndex = newVal;
      this.resetTransform();
    },
    visible(newVal) {
      if (!newVal) {
        this.showTip = false;
        this.tipMessage = '';
        this.removeKeyListener();
      } else {
        this.$nextTick(() => {
          this.addKeyListener();
        });
        this.$nextTick(() => { this.addKeyListener(); });
      }
    }
    },
  },
  methods: {
    close() {
      this.$emit('update:visible', false);
      this.$emit('close');
    },
    handleMaskClick() {
      this.close();
    },
    handleMaskClick() { this.close(); },
    // 切换图片
    goPrev() {
      if (this.isFirst) {
        this.showTipMessage('这已经是第一张了');
        return;
      }
      if (this.isFirst) { this.showTipMessage('已是第一张'); return; }
      this.currentIndex--;
      this.hideTip();
      this.resetTransform();
    },
    goNext() {
      if (this.isLast) {
        this.showTipMessage('这已经是最后一张了');
        return;
      }
      if (this.isLast) { this.showTipMessage('已是最后一张'); return; }
      this.currentIndex++;
      this.hideTip();
    },
    showTipMessage(message) {
      this.tipMessage = message;
      this.showTip = true;
      setTimeout(() => {
        this.hideTip();
      }, 2000);
    },
    hideTip() {
      this.showTip = false;
      this.tipMessage = '';
    },
    handleError() {
      console.error(`图片加载失败: ${this.currentUrl}`);
      this.resetTransform();
    },
    // 键盘事件处理
    handleKeydown(event) {
      switch(event.key) {
        case 'ArrowLeft':
          event.preventDefault(); // 阻止默认行为
          this.goPrev();
          break;
        case 'ArrowRight':
          event.preventDefault();
          this.goNext();
          break;
        case 'Escape':
          event.preventDefault();
          this.close();
          break;
    // 缩放
    zoomIn() {
      this.scale = Math.min(this.scale + 0.25, 5);
      if (this.scale <= 1) { this.offsetX = 0; this.offsetY = 0; }
    },
    zoomOut() {
      this.scale = Math.max(this.scale - 0.25, 0.25);
      if (this.scale <= 1) { this.offsetX = 0; this.offsetY = 0; }
    },
    handleWheel(e) {
      if (e.deltaY < 0) this.zoomIn();
      else this.zoomOut();
    },
    // 旋转
    rotateRight() { this.rotate = (this.rotate + 90) % 360; },
    rotateLeft() { this.rotate = (this.rotate - 90) % 360; },
    // 还原
    resetTransform() {
      this.scale = 1;
      this.rotate = 0;
      this.offsetX = 0;
      this.offsetY = 0;
    },
    // 拖拽(仅在放大时启用)
    handleMouseDown(e) {
      if (this.scale <= 1) return;
      this.dragging = true;
      this.dragStart = { x: e.clientX - this.offsetX, y: e.clientY - this.offsetY };
    },
    handleMouseMove(e) {
      if (!this.dragging) return;
      this.offsetX = e.clientX - this.dragStart.x;
      this.offsetY = e.clientY - this.dragStart.y;
    },
    handleMouseUp() {
      this.dragging = false;
    },
    showTipMessage(msg) {
      this.tipMessage = msg;
      this.showTip = true;
      setTimeout(() => { this.hideTip(); }, 2000);
    },
    hideTip() { this.showTip = false; this.tipMessage = ''; },
    handleError() { console.error('图片加载失败:', this.currentUrl); },
    // 键盘
    handleKeydown(e) {
      switch(e.key) {
        case 'ArrowLeft': e.preventDefault(); this.goPrev(); break;
        case 'ArrowRight': e.preventDefault(); this.goNext(); break;
        case 'Escape': e.preventDefault(); this.close(); break;
        case '+': case '=': e.preventDefault(); this.zoomIn(); break;
        case '-': e.preventDefault(); this.zoomOut(); break;
        case '0': e.preventDefault(); this.resetTransform(); break;
      }
    },
    addKeyListener() {
      document.addEventListener('keydown', this.handleKeydown);
    addKeyListener() { document.addEventListener('keydown', this.handleKeydown); },
    removeKeyListener() { document.removeEventListener('keydown', this.handleKeydown); },
    },
    removeKeyListener() {
      document.removeEventListener('keydown', this.handleKeydown);
    }
  },
  mounted() {
    if (this.visible) {
      this.addKeyListener();
    }
  },
  beforeUnmount() {
    this.removeKeyListener();
  }
  mounted() { if (this.visible) this.addKeyListener(); },
  beforeUnmount() { this.removeKeyListener(); },
};
</script>
<style scoped>
.custom-image-viewer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2000; /* 确保 z-index 足够高 */
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 2000;
}
.el-image-viewer__mask {
  position: absolute;
  width: 100%;
  height: 100%;
  position: absolute; width: 100%; height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
}
.el-image-viewer__btn {
  position: absolute;
  z-index: 1;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.3);
  border-radius: 50%;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  position: absolute; z-index: 1; color: #fff;
  background-color: rgba(0, 0, 0, 0.3); border-radius: 50%;
  width: 40px; height: 40px; display: flex;
  align-items: center; justify-content: center; cursor: pointer; user-select: none;
}
.el-image-viewer__btn.is-disabled { cursor: not-allowed; opacity: 0.5; }
.el-image-viewer__close { top: 20px; right: 20px; }
.el-image-viewer__prev { left: 20px; top: 50%; transform: translateY(-50%); }
.el-image-viewer__next { right: 20px; top: 50%; transform: translateY(-50%); }
.el-image-viewer__btn.is-disabled {
  cursor: not-allowed;
  opacity: 0.5;
.viewer-toolbar {
  position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
  z-index: 1; display: flex; align-items: center; gap: 8px;
  padding: 8px 16px; background: rgba(0, 0, 0, 0.6); border-radius: 24px;
}
.el-image-viewer__close {
  top: 20px;
  right: 20px;
.toolbar-btn {
  width: 36px; height: 36px; border-radius: 50%; display: flex;
  align-items: center; justify-content: center; cursor: pointer;
  color: #fff; font-size: 18px; transition: background 0.2s;
}
.el-image-viewer__prev {
  left: 20px;
  top: 50%;
  transform: translateY(-50%);
}
.el-image-viewer__next {
  right: 20px;
  top: 50%;
  transform: translateY(-50%);
}
.toolbar-btn:hover { background: rgba(255, 255, 255, 0.2); }
.toolbar-info { color: rgba(255, 255, 255, 0.7); font-size: 13px; margin-left: 8px; white-space: nowrap; }
.el-image-viewer__canvas {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  display: flex; justify-content: center; align-items: center;
  height: 100%; overflow: hidden;
}
.el-image-viewer__img {
  max-width: 90%;
  max-height: 90%;
  object-fit: contain;
  max-width: 90%; max-height: 90%;
  object-fit: contain; user-select: none; -webkit-user-drag: none;
}
.custom-viewer-tip {
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.7);
  color: #fff;
  padding: 10px 20px;
  border-radius: 4px;
  z-index: 2001;
  position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.7); color: #fff;
  padding: 10px 20px; border-radius: 4px; z-index: 2001;
}
</style>