canvas 在线批阅完整功能

252 阅读1分钟


<template>
  <div class="canvas_content">
    <div class="canvas_back">
      <div class="edit_content">
        <div class="paint_brush paint_color">
          <input type="color" v-model="colorModel" />
        </div>

        <div class="paint_brush paint_color">
          <img src="../../assets/images/画笔.png" alt="" />
          <img src="../../assets/images/画笔加粗.png" alt="" @click="add" />
          <div
            :style="{
              border: lineWidth + 'px solid #FFFFFF',
            }"
            class="border"
          ></div>
          <img src="../../assets/images/画笔减粗.png" alt="" @click="reduce" />
        </div>
        <div class="paint_brush paint_color">
          <img src="../../assets/images/文字.png" alt="" @click="handleEdit" />
          <a-select v-model="fontValue">
            <a-select-option
              v-for="(item, index) in fontSizeValue"
              :key="index"
              :value="item.name"
            >
              {{ item.name }}
            </a-select-option>
          </a-select>
        </div>

        <div class="paint_brush paint_color">
          <img src="../../assets/images/加粗.png" alt="" @click="handelBold" />
        </div>
        <div class="paint_brush paint_color">
          <img
            @click="rotateImg"
            src="../../assets/images/旋转@2x.png"
            alt=""
          />
        </div>
        <div class="paint_brush paint_color">
          <img
            src="../../assets/images/反撤销.png"
            alt=""
            @click="eraserReduce"
          />
          <img src="../../assets/images/撤销.png" alt="" @click="eraserAdd" />
        </div>
        <div class="paint_brush paint_color">
          <img src="../../assets/images/放大.png" alt="" @click="enlarge" />
          <img src="../../assets/images/缩小.png" alt="" @click="narrow" />
        </div>
      </div>
      <div class="canvas-box">
        <div class="canvas" :style="{ zoom }">
          <canvas
            @mousedown="mousedown($event)"
            ref="refCanvas"
            id="paintingId"
            :width="width"
            :height="height"
          ></canvas>
          <canvas
            @mousedown="mousedown($event)"
            ref="imgCanvas"
            id="imgCanvas"
            :width="width"
            :height="height"
          ></canvas>
          <div
            class="text-box"
            :class="boldStatus ? 'bold_font' : 'no-bold'"
            v-if="textBoxRect"
            v-focus
            contenteditable
            @input="textValueInput"
            :style="{
              left: textBoxRect.left + 'px',
              top: textBoxRect.top + 'px',
              height: fontHeight + 'px',
              fontSize: fontValue + 'px',
              fontWeight: boldStatus ? '600' : 'normal',
              color: colorModel,
            }"
          ></div>
        </div>
      </div>
      <div class="tips" v-if="showTipsStatus">
        编辑后的图片将保存于“批阅作业”区域
        <img
          @click="closeShowTips"
          src="../../assets/images/close-circle.png"
          alt=""
        />
      </div>
      <div class="button">
        <a-button type="link" style="color: #aeaeae" @click="$emit('close')">
          取消
        </a-button>
        <a-button type="link" @click="defineImg"> 保存批阅 </a-button>
      </div>
    </div>
  </div>
</template>

<script type="text/ecmascript-6">
import Shadow from '@/components/common/Shadow'
import BoxHeader from '@/components/common/BoxHeader'
import html2canvas from 'html2canvas'
import { getOssSign } from '@/apis/common'
import { formatUploadFile } from '@/assets/js/Common'
import { getRecordEdit } from '@/apis/homework'
import { State, Getter, Action, Mutation, namespace } from 'vuex-class'
import file from '@/mixin/common/file'

// const homeWorkStore = namespace('homeWork')
export default {
  name: '',
  components: { Shadow, BoxHeader },
  data() {
    return {
      title: '编辑',
      eraser: null,
      canvas: null,
      ctx: null,
      imgCtx: null,
      imgCanvas: null,
      lineWidth: 1,
      // strokeStyle: '#F00505',
      mode: '',
      textBoxRect: null,
      showTipsStatus: true,
      textValue: '',
      width: 800,
      height: 600,
      zoom: 1,
      imgStatus: {
        rotate: -1,
        width: 0,
        height: 0,
        originWidth: 0,
        originHeight: 0,
      },
      imgRatio: 1,
      colorModel: '#F00505',
      step: -1,
      canvasHistory: [],
      fontValue: '14',
      fontSizeValue: [
        {
          name: '8',
          id: 1,
        },
        {
          name: '10',
          id: 2,
        },
        {
          name: '12',
          id: 3,
        },
        {
          name: '14',
          id: 4,
        },
        {
          name: '16',
          id: 5,
        },
        {
          name: '18',
          id: 6,
        },
        {
          name: '24',
          id: 7,
        },
        {
          name: '36',
          id: 8,
        },
      ],
      boldStatus: false,
      uploadSign: null,
      fontHeight: null,
      ossConfig: {
        path: 'homeworkdetail/',
        host: '上传oss的地址',
        bucket: '名字',
      },
      rotateStatua: null,
    }
  },
  mixins: [file],
  computed: {},
  head() {
    return {}
  },
  props: ['imgPainting', 'imgObject', 'modifyImgObj'],
  watch: {},
  created() {},
  mounted() {
    this.initState()
    if (
      sessionStorage.getItem('showTipsStatus') &&
      !JSON.parse(sessionStorage.getItem('showTipsStatus'))
    ) {
      this.showTipsStatus = JSON.parse(sessionStorage.getItem('showTipsStatus'))
    }
  },
  watch: {
    fontValue(val) {
      if (val === 18) {
        this.fontHeight = 30
      }
    },
  },
  directives: {
    focus: {
      inserted(el) {
        setTimeout(() => {
          el.focus()
        }, 100)
      },
      componentUpdated(el) {
        setTimeout(() => {
          el.focus()
        }, 100)
      },
    },
  },
  methods: {
    calcImg() {
      const status = this.imgStatus
      const { originWidth, originHeight } = status
      status.rotate = (status.rotate + 1) % 4
      // 计算图片尺寸
      const baseWidth = status.rotate % 2 ? this.height : this.width
      const baseHeight = status.rotate % 2 ? this.width : this.height
      const ratio = originWidth / (originHeight || 1)
      status.width = ratio > 1 ? baseWidth : baseHeight * ratio
      status.height = status.width / ratio
      // 计算图片定位
      switch (status.rotate) {
        case 0:
          status.x = (this.width - status.width) / 2
          status.y = (this.height - status.height) / 2
          break
        case 1:
          status.x = (this.height - status.width) / 2
          status.y = -(status.height + this.width) / 2
          break
        case 2:
          status.x = -(this.width + status.width) / 2
          status.y = -(this.height + status.height) / 2
          break
        case 3:
          status.x = -(this.height + status.width) / 2
          status.y = (this.width - status.height) / 2
          break
      }
      console.log('status', status)
    },
    initState() {
      //  初始化图片
      this.imgCanvas = this.$refs.imgCanvas
      this.canvas = this.$refs.refCanvas
      this.imgCtx = this.imgCanvas.getContext('2d')
      this.ctx = this.canvas.getContext('2d')
      const image = new Image()
      image.onload = () => {
        this.imgStatus.originWidth = image.width
        this.imgStatus.originHeight = image.height
        this.calcImg()
        this.imgCtx.translate(this.imgStatus.x, this.imgStatus.y)
        this.imgCtx.drawImage(
          image,
          0,
          0,
          this.imgStatus.width,
          this.imgStatus.height
        )
      }
      image.setAttribute('crossOrigin', 'anonymous')
      image.src = this.imgPainting
    },
    narrow() {
      if (this.zoom < 1.1) return
      this.zoom -= 0.1
    },
    enlarge() {
      this.zoom += 0.1
    },
    rotateImg() {
      // 旋转图片
      const image = new Image()
      image.onload = () => {
        this.imgCtx.clearRect(
          -16,
          -16,
          this.imgStatus.width + 32,
          this.imgStatus.height + 32
        )
        this.imgCtx.translate(-this.imgStatus.x, -this.imgStatus.y)
        this.imgCtx.rotate(Math.PI / 2)
        this.calcImg()
        this.imgCtx.translate(this.imgStatus.x, this.imgStatus.y)
        this.imgCtx.drawImage(
          image,
          0,
          0,
          this.imgStatus.width,
          this.imgStatus.height
        )
      }
      image.setAttribute('crossOrigin', 'anonymous')
      image.src = this.imgPainting
      if (this.rotateStatua >= 3) {
        this.rotateStatua = null
      } else {
        this.rotateStatua += 1
      }
    },

    closeShowTips() {
      // 关闭窗口
      this.showTipsStatus = false
      JSON.stringify(
        sessionStorage.setItem('showTipsStatus', this.showTipsStatus)
      )
      // this.changeIsShowTips(this.showTipsStatus)
    },
    mousedown(e) {
      // 计算鼠标在画布的距离
      if (this.step < 0) {
        this.step++
        this.canvasHistory.push(this.canvas.toDataURL()) // 添加新的绘制到历史记录
      }
      // const disX = e.clientX - this.canvas.offsetX / this.zoom
      // const disY = e.clientY - (this.canvas.offsetY / this.zoom + 50)
      const disX = e.offsetX / this.zoom
      const disY = e.offsetY / this.zoom
      // 输入框
      if (this.mode === 'text') {
        if (this.textValue) {
          this.drawText()
          this.canvasHistory.push(this.canvas.toDataURL()) // 添加新的绘制到历史记录
        } else {
          this.textBoxRect = {
            left: disX,
            top: disY,
          }
        }
        // return
      }
      // 每次必须重新开始,让它们变成多个。
      this.ctx.beginPath()
      // 设置画线的宽,与颜色
      this.ctx.lineWidth = this.lineWidth
      this.ctx.strokeStyle = this.colorModel
      // 设置画的起始点
      this.ctx.moveTo(disX, disY)
      document.addEventListener('mousemove', this.mousemove)
      document.addEventListener('mouseup', this.mouseup)
    },
    canvasTextAutoLit() {
      /*
      str:要绘制的字符串
      canvas:canvas对象
      initX:绘制字符串起始x坐标
      initY:绘制字符串起始y坐标
      lineHeight:字行高,自己定义个值即可
      */
      let lineWidth = 0
      let lastSubStrIndex = 0 //每次开始截取的字符串的索引
      let initHeight = 15 //绘制字体距离canvas顶部初始的高度
      let maxWidth = 200
      for (let i = 0; i < this.textValue.length; i++) {
        lineWidth += this.ctx.measureText(this.textValue[i]).width
        console.log(this.textBoxRect.left, lineWidth)
        if (this.textBoxRect.left > 710) {
          maxWidth = 40
        }
        if (this.textBoxRect.left > 660 && this.textBoxRect.left < 710) {
          maxWidth = 80
        }
        if (this.textBoxRect.left < 670 && this.textBoxRect.left > 600) {
          maxWidth = 150
        }
        if (this.textBoxRect.left > 730) {
          this.ctx.fillText(
            this.textValue.substring(lastSubStrIndex, i),
            this.textBoxRect.left,
            this.textBoxRect.top + initHeight
          ) //绘制截取部分
          initHeight += 20 //20为字体的高度
          lineWidth = 0
          lastSubStrIndex = i
        }
        if (lineWidth > maxWidth) {
          this.ctx.fillText(
            this.textValue.substring(lastSubStrIndex, i),
            this.textBoxRect.left,
            this.textBoxRect.top + initHeight
          ) //绘制截取部分
          initHeight += 20 //20为字体的高度
          lineWidth = 0
          lastSubStrIndex = i
        }
        if (i == this.textValue.length - 1) {
          //绘制剩余部分
          this.ctx.fillText(
            this.textValue.substring(lastSubStrIndex, i + 1),
            this.textBoxRect.left,
            this.textBoxRect.top + initHeight
          )
        }
      }
    },
    drawText() {
      // 输入文字
      this.ctx.moveTo(this.textBoxRect.left, this.textBoxRect.top)
      this.ctx.fillStyle = this.colorModel
      this.ctx.font = `${this.boldStatus ? '600' : 'normal'} ${
        this.fontValue
      }px "微软雅黑"`
      this.ctx.textBaseline = 'top'
      this.ctx.textAlign = 'left'
      this.ctx.lineWidth = 1
      this.canvasTextAutoLit()
      this.textBoxRect = null
      this.mode = ''
      this.textValue = ''
      this.boldStatus = false
    },
    handelBold() {
      // 文字加粗
      this.boldStatus = !this.boldStatus
      console.log(this.boldStatus)
    },
    mousemove(e) {
      // 鼠标移动画线
      // const disX = e.clientX - this.canvas.offsetLeft
      // const disY = e.clientY - (this.canvas.offsetTop + 50)
      // const disX = e.clientX - this.canvas.offsetX / this.zoom
      // const disY = e.clientY - (this.canvas.offsetY / this.zoom + 50)
      const disX = e.offsetX / this.zoom
      const disY = e.offsetY / this.zoom
      // 移动时设置画线的结束位置。并且显示
      this.ctx.lineTo(disX, disY) // 鼠标点下去的位置
      this.ctx.stroke()
    },
    mouseup() {
      // 鼠标离开时记录
      this.step++
      if (this.step < (this.canvasHistory && this.canvasHistory.length)) {
        this.canvasHistory.length = this.step // 截断数组
      }
      document.removeEventListener('mousemove', this.mousemove)
      document.removeEventListener('mouseup', this.mouseup)
      this.canvasHistory.push(this.canvas.toDataURL()) // 添加新的绘制到历史记录
    },
    add() {
      // 画笔加粗
      if (this.lineWidth >= 6) return
      this.lineWidth++
    },
    reduce() {
      // 画笔粗细
      if (this.lineWidth > 1) {
        this.lineWidth--
      }
    },
    eraserAdd() {
      // 撤销
      if (this.step <= this.canvasHistory.length - 1) {
        this.step++
        let canvasPic = new Image()
        canvasPic.src = this.canvasHistory[this.step]
        canvasPic.addEventListener('load', () => {
          this.ctx.clearRect(0, 0, this.width, this.height)
          this.ctx.drawImage(canvasPic, 0, 0)
        })
      } else {
        this.$message.info('已经是最新的操作了')
      }
    },
    eraserReduce() {
      // 反撤销
      if (this.step > 0) {
        this.step--
        this.ctx.clearRect(0, 0, this.width, this.height)
        let canvasPic = new Image()
        canvasPic.src = this.canvasHistory[this.step]
        canvasPic.addEventListener('load', () => {
          this.ctx.drawImage(canvasPic, 0, 0)
        })
      } else {
        this.$message.info('不能再继续撤销了')
      }
    },
    async defineImg() {
      // 保存批阅完成后的图片
      let flag = true
      if (flag) {
        flag = false
        this.imgCtx.translate(-this.imgStatus.x, -this.imgStatus.y)
        this.imgCtx.rotate((-this.imgStatus.rotate * Math.PI) / 2)
        this.imgCtx.drawImage(this.canvas, 0, 0)
        let blob = this.getBlob(this.imgCanvas)
        if (
          !this.fileTypes.imgTypes.some(
            (type) => String(this.imgObject.fileName).indexOf(type) !== -1
          )
        ) {
          this.imgObject.fileName =
            this.imgObject.fileName + '.' + this.imgObject.fileType
        }
        let fileOfBlob = new window.File([blob], this.imgObject.fileName, {
          type: this.imgObject.fileType,
        })

        let fileUploadInfo = await this.ossSign(fileOfBlob)
        let obj = {
          file: {
            name: this.imgObject.fileName,
            size: this.imgObject.fileSize,
            type: this.imgObject.fileType,
          },
          uuid: fileUploadInfo.uuid,
          url: fileUploadInfo.fileList,
          fileUrl:
            'oss地址' +
            fileUploadInfo.fileList,
        }
        this.$emit('defineEditImg', obj)
        this.getRecordEdit()
        flag = true
      }
    },
    async getRecordEdit() {
      let res = await this.$axios.post(getRecordEdit, this.modifyImgObj)
      if (res.status === 200 && res.data.code === 200) {
        console.log('埋点操作成功')
      }
    },
    async ossSign(list) {
      // 上传oss
      const res = await this.$axios.post(getOssSign, this.ossConfig)
      const promiseArr = []
      // for (let i = 0; i < list.length; i++) {
      const fileName = list.name
      const formatFile = formatUploadFile(list, res.data.data, fileName)
      const resp = this.$axios.post(res.data.data.host, formatFile, {
        headers: { 'Content-Type': 'multipart/form-data' },
        withCredentials: false,
      })
      promiseArr.push(resp)
      // }
      try {
        const fileList = await Promise.all(promiseArr).then((arr) => {
          return `${res.data.data.dir}/${this.imgObject.fileName}`
        })
        console.log(res.data.data.dir, this.imgObject.fileName)
        return { fileList, uuid: res.data.data.dir.split('/')[1] }
      } catch (error) {
        console.log(error)
      }
    },
    getBlob(canvas) {
      //获取blob对象
      var data = canvas.toDataURL('image/png', 1)
      this.picdown = data
      data = data.split(',')[1]
      data = window.atob(data)
      var ia = new Uint8Array(data.length)
      for (var i = 0; i < data.length; i++) {
        ia[i] = data.charCodeAt(i)
      }
      return new Blob([ia], {
        type: 'image/png',
      })
    },
    downloadFile(blob, fileName) {
      // 下载文件,暂时不用
      let aLink = document.createElement('a')
      let evt = document.createEvent('HTMLEvents')
      evt.initEvent('click', true, true) //initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
      aLink.download = fileName
      aLink.href = URL.createObjectURL(blob)
      aLink.click()
    },
    handleEdit() {
      // 点击输入文字
      if (this.mode === 'text') {
        this.mode = ''
        this.textBoxRect = null
      } else {
        this.mode = 'text'
      }
    },
    textValueInput(e) {
      // 输入文字的输入框
      this.textValue = e.target.innerText
    },
  },
}
</script>

<style scoped lang="scss">
.canvas_content {
  position: fixed;
  top: 0;
  left: 0;
  /* right: 0; */
  /* bottom: 0; */
  z-index: 999;
  // transform: translate(-50%, -50%);
}
.canvas_back {
  width: 800px;
  background: rgba(51, 51, 51, 1);
}
.edit_content {
  height: 51px;
  background: #333333;
  box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  .paint_brush {
    display: flex;
    align-items: center;
    img {
      cursor: pointer;
      margin-right: 10px;
    }
    .ant-select-selection {
      width: 60px;
    }
  }
  .paint_color {
    margin-right: 24px;
  }
}

.canvas-box {
  width: 800px;
  height: 610px;
  overflow: auto;
}
@media only screen and (max-height: 768px) {
  .canvas-box {
    height: 520px;
  }
}
.canvas {
  position: relative;
  #paintingId {
    position: relative;
    z-index: 1;
  }
  .text-box {
    border: 1px solid #333;
    position: absolute;
    outline: none;
    background-color: transparent;
    max-width: 200px;
  }
  #imgCanvas {
    position: absolute;
    top: 0px;
    left: 0px;
    z-index: 0;
  }
}
.tips {
  position: relative;
  background: rgba(253, 204, 119, 0.52);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ffffff;
  img {
    cursor: pointer;
    position: absolute;
    right: 10px;
    top: 2px;
  }
}
.button {
  display: flex;
  align-items: center;
  justify-content: center;
}
.border {
  width: 15px;
  color: #ffffff;
  border: 1px solid #ffffff;
  margin-right: 10px;
}
// .feedback-content {
//   position: relative;
//   .canvas_content {
//     // position: relative;
//     .text-mode {
//       cursor: text;
//     }
// .text-box {
//   // min-width: 100px;
//   // min-height: 20px;
//   border: 1px solid #333;
//   position: absolute;
//   outline: none;
//   background-color: transparent;
// }
//     .bold_font {
//       font-weight: 600;
//     }
//     .no-bold {
//       font-weight: normal;
//     }
//   }
//   .operation_button {
//     height: 100px;
//     input {
//       width: 70px;
//       height: 50px;
//       font-size: 20px;
//     }
//   }
// }
</style>