[vue2] 图片标记器

​​​​​​

  • 鼠标在图片上移动时显示“十字准星”
  • 长按+拖拽+松开鼠标左键,展示一个框
  • 点击选中框,被选中框有不同的颜色(下面示例中,自动选择了新生成的框)

 最终效果示例:

说明:

  • 自己造的轮子,不算完美,可能有疏忽的细节
  • 已经处理了页面尺寸自适应,图片尺寸取决于父组件的容器大小,框的数据是相对于图片宽高的比例(0-1之间数字)
  • 因为一开始需要读取图片的宽高,所以需要给图片设置id。也就是说,如果需要在同一页面使用2个以上这个组件,需要自己去把dom id打包,避免多个图片使用同一id。

 组件代码:

<template>
  <div
    @mousedown="handleMouseDown"
    @mouseup="handleMouseUp"
    @mousemove="handleMouseMove"
    @mouseover="handleMouseOver"
    @mouseleave="handleMouseLeave"
    class="container"
  >
    <img
      v-if="imgPath.length"
      id="img-marking"
      :src="imgPath"
      class="auto-size"
      draggable="false"
      @load="setSize"
    />
    <div
      v-show="showCross"
      class="cross cross-vertical"
      :style="{
        height: `${crossHeight}px`,
        top: `${containerTop}px`,
        left: `${mouseX}px`,
      }"
    ></div>
    <div
      v-show="showCross"
      class="cross cross-horizontal"
      :style="{
        width: `${crossWidth}px`,
        top: `${mouseY}px`,
        left: `${containerLeft}px`,
      }"
    ></div>
    <template v-if="imgLoaded">
      <div
        v-for="(item, i) in rectList"
        :key="i"
        :class="i === selectedRectIndex ? 'rect rect-red' : 'rect rect-blue'"
        :style="{
          top: `${item.top * containerHeight + containerTop}px`,
          left: `${item.left * containerWidth + containerLeft}px`,
          width: `${(item.right - item.left) * containerWidth}px`,
          height: `${(item.bottom - item.top) * containerHeight}px`,
        }"
        @click.stop="rectClick(i)"
      ></div>
      <div
        v-show="showDrawingRect"
        class="rect rect-red"
        :style="{
          top: `${
            Math.min(drawingPosition.startY, drawingPosition.endY) +
            containerTop
          }px`,
          left: `${
            Math.min(drawingPosition.startX, drawingPosition.endX) +
            containerLeft
          }px`,
          width: `${Math.abs(drawingPosition.startX - drawingPosition.endX)}px`,
          height: `${Math.abs(
            drawingPosition.startY - drawingPosition.endY
          )}px`,
        }"
      ></div>
    </template>
  </div>
</template>
<script>
export default {
  name: "ImageMarker",
  props: {
    imgPath: {
      type: String,
      required: true,
    },
    rectList: {
      type: Array,
      required: true,
    },
    selectedRectIndex: {
      type: Number,
      required: true,
    },
    minimumSize: {
      type: Array,
      default: () => [50, 50],
    },
  },
  data() {
    return {
      imgLoaded: false,
      showCross: false,
      crossHeight: 0,
      crossWidth: 0,
      containerLeft: 0,
      containerRight: 0,
      containerTop: 0,
      containerBottom: 0,
      containerWidth: 0,
      containerHeight: 0,
      mouseX: 0,
      mouseY: 0,
      mouseOffset: 5,
      lastMouseDown: [0, 0],
      drawingRect: false,
      drawingPosition: {},
    };
  },
  computed: {
    showDrawingRect() {
      if (!this.drawingRect) {
        return false;
      }
      if (
        Math.abs(this.drawingPosition.startY - this.drawingPosition.endY) < 8 &&
        Math.abs(this.drawingPosition.startY - this.drawingPosition.endY) < 8
      ) {
        return false;
      }
      return true;
    },
  },
  mounted() {
    window.addEventListener("resize", this.setSize);
  },
  unmounted() {
    window.removeEventListener("resize", this.setSize);
  },
  methods: {
    // 根据图片尺寸设置准星长度+尺寸自适应
    setSize() {
      this.imgLoaded = true;
      const container = document.getElementById("img-marking");
      if (!container) return;
      const { top, bottom, left, right } = container.getBoundingClientRect();
      this.crossHeight = bottom - top;
      this.crossWidth = right - left;
      this.containerTop = top;
      this.containerBottom = bottom;
      this.containerLeft = left;
      this.containerRight = right;
      this.containerWidth = right - left;
      this.containerHeight = bottom - top;
    },
    // 鼠标移动
    handleMouseMove(e) {
      this.showCross =
        e.clientX < this.containerRight && e.clientY < this.containerBottom;
      if (!this.showCross) {
        return;
      }
      this.mouseX = Math.max(e.clientX - this.mouseOffset, this.containerLeft);
      this.mouseY = Math.max(e.clientY - this.mouseOffset, this.containerTop);
      if (this.drawingRect) {
        this.drawingPosition = {
          ...this.drawingPosition,
          endX: this.mouseX - this.containerLeft,
          endY: this.mouseY - this.containerTop,
        };
      }
    },
    // 鼠标移进
    handleMouseOver() {
      this.showCross = true;
    },
    // 鼠标移出
    handleMouseLeave() {
      this.showCross = false;
    },
    // 鼠标按下
    handleMouseDown(e) {
      this.drawingRect = true;
      this.lastMouseDown = [e.clientX, e.clientY];
      this.drawingPosition = {
        startX: this.mouseX - this.containerLeft - this.mouseOffset,
        startY: this.mouseY - this.containerTop - this.mouseOffset,
        endX: this.mouseX - this.containerLeft - this.mouseOffset,
        endY: this.mouseY - this.containerTop - this.mouseOffset,
      };
      console.log(this.rectList);
    },
    // 鼠标按起
    handleMouseUp(e) {
      this.drawingRect = false;
      if (
        Math.abs(e.clientX - this.lastMouseDown[0]) < this.minimumSize[0] ||
        Math.abs(e.clientY - this.lastMouseDown[1]) < this.minimumSize[1]
      ) {
        return;
      }
      const { startX, startY, endX, endY } = this.drawingPosition;
      const list = this.rectList;
      list.push({
        top: Math.min(startY, endY) / this.containerHeight,
        bottom: Math.max(startY, endY) / this.containerHeight,
        left: Math.min(startX, endX) / this.containerWidth,
        right: Math.max(startX, endX) / this.containerWidth,
      });
      this.$emit("update:rectList", list);
      this.$emit("update:selectedRectIndex", list.length - 1);
    },
    // 点击方块
    rectClick(i) {
      this.$emit("update:selectedRectIndex", i);
    },
  },
};
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
  user-select: none;
}

.auto-size {
  max-width: 100%;
  max-height: 100%;
}

.cross {
  position: absolute;
  background: #f85757;
  z-index: 99;
}

.cross-vertical {
  width: 2px;
}

.cross-horizontal {
  height: 2px;
}

.rect {
  position: absolute;
  z-index: 50;
}

.rect-red {
  border: 1px solid rgb(125, 5, 5);
  background: rgba(125, 5, 5, 0.3);
}

.rect-blue {
  border: 1px solid rgb(3, 38, 165);
  background: rgba(3, 38, 165, 0.3);
}
</style>

引用示例:

  • imgPath: 图片路径
  • rectList: 所有框的位置数据
  • selectedRectIndex当前选择的框的索引(rectList里的)
  • minimumSize: 每个框的最小[宽,高] 
<template>
  <div style="width: 1200px; height: 900px">
    <ImageMarker
      :imgPath="imgPath"
      :rectList.sync="rectList"
      :selectedRectIndex.sync="selectedRectIndex"
      :minimumSize="[50, 50]"
    />
  </div>
</template>

<script>
import ImageMarker from "@/components/ImageMarker.vue";

export default {
  name: "HomeView",
  components: {
    ImageMarker,
  },
  data() {
    return {
      imgPath: "",
      rectList: [],
      selectedRectIndex: -1,
    };
  },
  mounted() {
    this.imgPath = "test.jpg";
    this.rectList = [
      {
        bottom: 0.81446331360189,
        left: 0.2829747427502339,
        right: 0.5028063610851263,
        top: 0.3749187064749361,
      },
      {
        bottom: 0.7,
        left: 0.55,
        right: 0.9,
        top: 0.6,
      },
    ];
    this.selectedRectIndex = 1;
  },
};
</script>

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

该写代码了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值