接上一篇文章 图片标注功能,这次对vue项目中对图片标注功能,在图片上绘制红色矩形框并且可以在矩形框上添加文字说明,进行了功能的细化,逻辑上的处理,增加了一些功能,完善了一些,处理的一些存在的问题。下面对这个某块进行详细的功能说明。
在图片上进行红色矩形框的绘制并且在上边框添加文字,同时可以对一张图片进行多个标注,可以获取标注信息保存起来,存到数据库。等下次加载把标注信息再复现到图片上,可以对标注的矩形框进行删除。
页面布局如图所示:在这里插入图片描述

1、非编辑模式下禁用清除、保存,文字添加功能;
2、编辑模式下,首先会获取对应图片的标注。有标注信息就绘制在图片上。
3、鼠标按下收起绘制好了一个矩形,可以输入文字描述,点击添加可以对红色矩形框标注添加文字说明。鼠标选中哪一个红色矩形框标注就可以给哪个红色矩形框添加说明。
4、鼠标点击图片上的红色矩形框标注,按键盘backspace可以删除该矩形框。
5、点击清除,会清除图片上临时的红色矩形框标注信息,然后点击保存才是真正对图片上的标注进行了清除;
6、点击退出编辑:会判断上次的标注信息和这次是否一样,一样就直接退出。不一样的话弹出提示,还未保存是否退出?确定的话就直接退出,新增加的红色矩形框标注会清理,保存之前的。取消的话不做任何操作。
7、编辑模式下,不允许切换照片。非编辑模式下,切换照片后,会绘制当前照片所保存的标注信息。
说明:该功能还存在一些瑕疵,等有空了继续修改。
代码如下:
这是html

<template>
  <div id="markBox">
    <div class="box-top">
      <button class="btn-item" @click="toggleEditing">
        {{ isEditing ? "退出编辑" : "开始绘制" }}
      </button>
      <button class="btn-item" @click="clearRectangles" :disabled="!isEditing">
        清除
      </button>
      <button class="btn-item" @click="saveRectangles" :disabled="!isEditing">
        保存
      </button>
    </div>
    <div class="box-bottom">
      <div class="box-bottom-top">
        <data
          class="img-item"
          :class="{ 'img-item-active': curPic == item.id }"
          v-for="(item, index) in srcList"
          :key="index"
          @click="cilckPic(item)"
        >
          <img :src="item.src" alt="" />
        </data>
      </div>
      <div class="box-bottom-bottom">
        <div class="inputbox">
          <input
            type="text"
            v-model="description"
            placeholder="输入描述文字"
            :disabled="!isEditing"
            @focus="isInputFocus = true"
            @blur="isInputFocus = false"
          />
          <button
            class="add-btn"
            @click="updateDescription"
            :disabled="!isEditing"
          >
            添加
          </button>
        </div>
        <canvas
          ref="canvas"
          :width="imageWidth"
          :height="imageHeight"
          @mousedown="onMouseDown"
          @mousemove="drawRect"
          @mouseup="endDrawing"
        ></canvas>
        <img
          :src="imageSrc"
          alt="Image"
          @load="initializeCanvas"
          style="display: none"
        />
      </div>
    </div>
  </div>
</template>

这是javascript


export default {
  name: "mark",
  data() {
    return {
      srcList: [
        { src: require("@/assets/images/1.jpg"), id: 1 },
        { src: require("@/assets/images/2.jpg"), id: 2 },
        { src: require("@/assets/images/3.jpg"), id: 3 },
        { src: require("@/assets/images/4.jpg"), id: 4 },
        { src: require("@/assets/images/5.jpg"), id: 5 },
      ],
      curPic: 1,
      imageSrc: require("@/assets/images/1.jpg"), // 确保图片路径正确
      imageWidth: 600, // 根据你的图片宽度调整
      imageHeight: 400, // 根据你的图片高度调整
      isDrawing: false,
      startX: 0,
      startY: 0,
      ctx: null,
      img: new Image(),
      rectangles: [], // 用于存储矩形框信息
      selectedRectIndex: null, // 选中的矩形框索引
      isEditing: false, // 编辑模式状态
      description: "", //文字描述
      isSaved: false, //是否保存了
      isInputFocus: false, //输入框正在输入
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.loadRectangles(); // 加载保存的矩形框信息
    });
  },
  methods: {
    cilckPic(item) {
      if (this.isEditing) {
        this.$message("正在编辑不可切换");
        return;
      }
      this.curPic = item.id;
      this.imageSrc = item.src;
      //切换图片时清除上一张的绘制效果
      this.clearRectangles();
      //获取本地是否已经绘制过当前图片
      this.loadRectangles();
    },
    //图片加载获取canvas画板,绘制图片,同时绘制标注(如果存在)
    initializeCanvas() {
      const canvas = this.$refs.canvas;
      this.ctx = canvas.getContext("2d");
      this.img.src = this.imageSrc;

      this.img.onload = () => {
        this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
        this.redrawRectangles();
      };
    },
    toggleEditing() {
      //进入编辑状态,设置isSave为false
      if (this.isEditing == false) {
        this.isSaved = false;
        this.isEditing = !this.isEditing;
      } else {
        //判断标注是否有变化:
        // 1、无变化,直接退出编辑,然后提示无变化
        // 2、有变化,提示是否保存;选择是,保存退出;选择清除绘制,加载已有的
        const nowRectangles = JSON.stringify(this.rectangles);
        const savedRectangles = localStorage.getItem(
          "savedRectangles" + this.curPic
        );
        if (nowRectangles == savedRectangles) {
          this.isEditing = !this.isEditing;
          this.$message.warning("未作修改");
          return;
        }
        if (this.isSaved) {
          this.isEditing = !this.isEditing;
        } else {
          this.$confirm("还未保存是否退出?", "提示", {
            confirmButtonText: "确定",
            cancelButtonText: "取消",
            type: "warning",
          })
              .then(() => {
              //图片上的全部清除,然后读取存在的
              this.clearRectangles();
              this.loadRectangles();
              this.isEditing = !this.isEditing;
            })
            .catch(() => {
            });
        }
      }
    },
    //鼠标放下
    onMouseDown(event) {
      if (!this.isEditing) return;

      const x = event.offsetX;
      const y = event.offsetY;

      // 检查点击位置是否在某个矩形框内
      this.selectedRectIndex = this.rectangles.findIndex((rect) => {
        return (
          x >= rect.x &&
          x <= rect.x + rect.width &&
          y >= rect.y &&
          y <= rect.y + rect.height
        );
      });

      if (this.selectedRectIndex === -1) {
        // 如果没有选中矩形框,开始绘制新矩形框
        this.isDrawing = true;
        this.startX = x;
        this.startY = y;
      } else {
        // 如果选中矩形框,监听键盘事件
        window.addEventListener("keydown", this.onKeyDown);
      }
    },
    //鼠标移动过程中
    drawRect(event) {
      if (!this.isDrawing) return;
      const canvas = this.$refs.canvas;
      this.ctx.clearRect(0, 0, canvas.width, canvas.height);
      this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);

      // 绘制已有的矩形框
      this.redrawRectangles();

      // 绘制当前矩形框
      const currentX = event.offsetX;
      const currentY = event.offsetY;
      const width = currentX - this.startX;
      const height = currentY - this.startY;

      this.ctx.strokeStyle = "red";
      this.ctx.setLineDash([5, 3]);
      this.ctx.lineWidth = 2;
      this.ctx.strokeRect(this.startX, this.startY, width, height);
    },
    endDrawing(event) {
      if (!this.isDrawing) return;

      this.isDrawing = false;
      const currentX = event.offsetX;
      const currentY = event.offsetY;
      const width = currentX - this.startX;
      const height = currentY - this.startY;
      if (width == 0 || height == 0) {
        //如果宽或高为0,不绘制
        return;
      }
      // 保存当前矩形框信息
      this.rectangles.push({
        x: this.startX,
        y: this.startY,
        width: width,
        height: height,
        description: "", // 初始化描述文字
      });
      //此处表述,当某绘制完成的矩形
      this.selectedRectIndex = this.rectangles.length - 1;
    },
    onKeyDown(event) {
      if (this.isInputFocus) {
        return;
      }
      if (event.key === "Backspace" && this.selectedRectIndex !== null) {
        // 删除选中的矩形框
        this.rectangles.splice(this.selectedRectIndex, 1);
        this.selectedRectIndex = null;

        // 重新绘制矩形框
        this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
        this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
        this.redrawRectangles();

        // 取消键盘事件监听
        window.removeEventListener("keydown", this.onKeyDown);
      }
    },
    clearRectangles() {
      this.rectangles = [];
      this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
      this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
    },
    saveRectangles() {
      localStorage.setItem(
        "savedRectangles" + this.curPic,
        JSON.stringify(this.rectangles)
      );
      this.isSaved = true;
      this.$message.success("矩形框已保存到 localStorage");
    },
    loadRectangles() {
      const canvas = this.$refs.canvas;
      this.ctx = canvas.getContext("2d");
      const savedRectangles = localStorage.getItem(
        "savedRectangles" + this.curPic
      );
      if (savedRectangles) {
        this.rectangles = JSON.parse(savedRectangles);
        this.redrawRectangles();
      }
    },
    updateDescription() {
      if (!this.rectangles[0]) {
        this.$message.warning("未绘制标注");
        return;
      }
      if (this.selectedRectIndex == null || this.selectedRectIndex == -1) {
        this.$message.warning("未选择标注框");
        return;
      }
      if (!this.description.trim()) {
        this.$message.warning("描述信息不可为空");
        return;
      }
      this.rectangles[this.selectedRectIndex].description = this.description;
      // 重新绘制矩形框,更新描述文字
      this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
      this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
      this.redrawRectangles();
    },
    redrawRectangles() {
      this.rectangles.forEach((rect) => {
        this.ctx.strokeStyle = "red";
        this.ctx.setLineDash([5, 3]);
        this.ctx.lineWidth = 2;
        this.ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);

        // 绘制描述文字
        if (rect.description) {
          this.ctx.font = "16px Arial";
          this.ctx.fillStyle = "red";
          this.ctx.fillText(rect.description, rect.x, rect.y - 5);
        }
      });
      this.description = "";
    },
  },
};

这是css样式


#markBox {
  height: 100%;
  width: 100%;
  text-align: center;
  padding-top: 20px;
  .box-top {
    display: flex;
    align-items: center;
    justify-content: center;
    .btn-item {
      height: 30px;
      width: 100px;
      margin: 5px;
    }
  }
  .box-bottom {
    .box-bottom-top {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 10px;
      .img-item {
        width: 100px;
        height: 60px;
        margin: 0px 5px;
        cursor: pointer;
        img {
          width: 100%;
          height: 100%;
        }
      }
      .img-item-active {
        border: 1px solid #3fffff;
      }
    }
    .box-bottom-bottom {
      .inputbox {
        display: flex;
        justify-content: center;
        align-items: center;
        padding-bottom: 10px;
        input {
          margin: 0;
          width: 180px;
          height: 30px;
          box-sizing: border-box;
          outline: none;
          border-top-left-radius: 5px;
          border-bottom-left-radius: 5px;
          border: 1px solid #ccc;
          padding-left: 5px;
        }
        .add-btn {
          height: 30px;
          width: 60px;
          border: 1px solid #ccc;
          border-top-right-radius: 5px;
          border-bottom-right-radius: 5px;
          border-left: none;
        }
      }
    }
    input {
      display: block;
      margin: 10px auto;
    }
    canvas {
      border: 1px solid black;
    }
  }
  //公共
  button[disabled] {
    cursor: not-allowed;
  }
  input[disabled] {
    cursor: not-allowed;
  }
}
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐