element-ui 自定义上传图片添加水印并上传至oss

随手笔记,这里我仅仅只是满足需求而封装的,如需使用还需要改进,这里只记一下方法。

  1. utils.js里封装打文字水印的方法
/**
 * 图片路径转成canvas
 * @param {图片url} url
 */
async function imgToCanvas(url) {
  // 创建img元素
  const img = document.createElement('img');
  img.src = url;
  img.setAttribute('crossOrigin', 'anonymous'); // 防止跨域引起的 Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
  await new Promise(resolve => {
    img.onload = resolve;
  });
  // 创建canvas DOM元素,并设置其宽高和图片一样
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // 坐标(0,0) 表示从此处开始绘制,相当于偏移。
  canvas.getContext('2d').drawImage(img, 0, 0);
  return canvas;
}

/**
 * canvas添加水印
 * @param {canvas对象} canvas
 * @param {水印文字} text
 */
function addWatermark(canvas, texts) {
  const [text1, text2, text3] = texts;
  const ctx = canvas.getContext('2d');
  const x = canvas.width - 20;
  const y = canvas.height - 20;
  ctx.fillStyle = 'red';
  ctx.textBaseline = 'middle';
  ctx.font = '30px';
  ctx.textAlign = 'end';
  ctx.fillText(text1, x, y);
  ctx.fillText(text2, x, y - 20);
  ctx.fillText(text3, x, y - 40);
  return canvas;
}

/**
 * canvas转成img
 * @param {canvas对象} canvas
 */
function convasToImg(canvas) {
  // 新建Image对象,可以理解为DOM
  const image = new Image();
  // canvas.toDataURL 返回的是一串Base64编码的URL
  // 指定格式 PNG
  image.src = canvas.toDataURL('image/png');
  return image;
}

// 封装方法
export async function watermark(imgUrl, texts) {
  // 1.图片路径转成canvas
  const tempCanvas = await imgToCanvas(imgUrl);
  // 2.canvas添加水印
  const canvas = addWatermark(tempCanvas, texts);
  // 3.canvas转成img
  const img = convasToImg(canvas);
  return img;
}
  1. 上传组件封装
    在我的项目中用到了
    高德地图定位 amap-js
    阿里云存储 ali-oss
    oss我进行了封装,这里就直接引入使用
<template>
  <div>
    <el-upload
      ref="upload"
      action
      list-type="picture-card"
      accept="image/jpeg, image/png"
      :limit="limit"
      :file-list="fileList"
      :http-request="httpRequest"
      :before-upload="beforeUpload"
      :before-remove="beforeRemove"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
      :on-success="handleSuccess"
      :on-error="handlEerror"
      :on-exceed="handlExceed"
      :disabled="disabled"
      :on-change="change"
      :auto-upload="true"
    >
      <i class="el-icon-plus"></i>
    </el-upload>
    <el-dialog width="80%" :close-on-click-modal="false" append-to-body :visible.sync="dialogVisible">
      <img class="bigImage" :src="dialogImageUrl" alt />
    </el-dialog>

  </div>
</template>

<script>
import { mapState } from 'vuex';
import { client } from '@/utils/oss';
import { watermark, amapLoader } from '@/utils/utils';
import hrApi from '@/api/hr';

export default {
  name: 'UploadPicture',
  components: {},
  props: {
    name: {
      type: String,
      required: true
    },
    extra: {
      type: Object,
      default: () => {}
    },
    fileList: {
      type: Array,
      required: true
    },
    limit: {
      type: Number,
      default: 10
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      dialogImageUrl: '',
      dialogVisible: false,

      fileUrl: '',
    };
  },
  computed: {
    ...mapState('app', ['taskId', 'busiId', 'enterpriseId', 'staffCode'])
  },
  watch: {},
  created() {},
  methods: {
    amapLoader,
    watermark,
    async beforeUpload(file) {
      const { type, size } = file;
      const accept = ['image/jpeg', 'image/png'];
      const isLt2M = size / 1024 / 1024 < 20;

      if (!accept.includes(type)) {
        this.$message.warning('只支持jpg、png格式的图片上传');
        return false;
      }

      if (!isLt2M) {
        this.$message.warning('上传图片大小不能超过 20MB');
        return false;
      }
      return true;
    },
    httpRequest(option) {
      const { file } = option;
      const { name } = file;

      const fileObj = {};
      const reader = new FileReader();
      reader.readAsDataURL(file); // 得到base64编码格式
      reader.onload = async e => { // 转成blob格式
        const url = URL.createObjectURL(this.dataURLtoBlob(e.currentTarget.result));
        fileObj.url = url;
        this.beforeUploadHandleImg(fileObj)
          .then(() => {
            // catalog_name/201909/29/filename_taskId_busiId_enterpriseId_毫秒级时间戳.type
            const root = this.name || 'unknown';

            const t = new Date();
            const y = t.getFullYear();
            const m = `${t.getMonth() + 1}`.padStart(2, '0');
            const d = `${t.getDate()}`.padStart(2, '0');

            const fileName = name.substring(0, name.indexOf('.'));
            const type = name.substring(name.lastIndexOf('.') + 1);

            const { taskId, busiId, enterpriseId } = this;
            const time = Date.now();
            const extra = `_${taskId}_${busiId}_${enterpriseId}_${time}`;

            const uuid = `${fileName}${extra}.${type}`;
            const path = `${root}/${y}${m}/${d}/${uuid}`;

            option.onProgress({
              percent: 0
            });
            const loading = this.$loading({
              lock: true,
              text: '图片上传中,请稍后',
              spinner: 'el-icon-loading',
              background: 'rgba(0, 0, 0, 0.7)'
            });
            client()
              .multipartUpload(path, this.fileUrl, {
                progress: async p => {
                  const e = {
                    percent: p * 100
                  };
                  option.onProgress(e);
                }
              })
              .then(result => {
                const res = result;
                res.url = `${process.env.VUE_APP_BUCKET}:${res.name}`;
                option.onSuccess(res);
              })
              .catch(err => {
                option.onError(err);
              })
              .finally(() => {
                loading.close();
              });
          })
          .catch(() => {
            option.onError();
          });
      };
    },
    beforeRemove(file) {
      const { status } = file;
      if (status === 'success') {
        const { name } = file;
        const arr = name.split('/');
        const filename = arr[arr.length - 1];
        return this.$confirm(`确定移除 ${filename}吗?`, '提示', {
          type: 'warning'
        });
      }
      return true;
    },
    handleRemove(file) {
      const { status } = file;
      if (status === 'success') {
        let name = '';
        if (file.response) {
          name = file.response.name;
        } else {
          name = file.name;
        }
        this.$emit('remove', name, this.extra);
      }
    },
    handleSuccess(result) {
      const { url } = result;
      this.$emit('success', url, this.extra);
    },
    handlEerror() {
      this.$message.error('上传失败');
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    handlExceed() {
      const { limit } = this;
      this.$message.warning(`最多可上传${limit}张图片,请先删除。`);
    },
    beforeUploadHandleImg(e) {
      const dom = document.createElement('div');
      dom.setAttribute('id', 'container');
      document.body.appendChild(dom);
      return new Promise((resolve, reject) => {
        this.$loading({
          lock: true,
          text: '图片处理中,请稍后',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });
        amapLoader().load()
          .then(({ AMap }) => {
          // 实例化容器
            const mapObj = new AMap.Map('container');
            // 使用地图插件-获取当前位置
            mapObj.plugin('AMap.Geolocation', () => {
              const geolocation = new AMap.Geolocation();
              // mapObj.addControl(geolocation);
              geolocation.getCurrentPosition((status, result) => {
                if (status === 'complete') {
                  const { lat, lng } = result.position;
                  // 通过经纬度逆解析地址
                  mapObj.plugin('AMap.Geocoder', () => {
                    const geocoder = new AMap.Geocoder();
                    geocoder.getAddress([lng, lat], (cstatus, cresult) => {
                      if (cstatus === 'complete' && cresult.regeocode) {
                        const { province, city, district, township, street, streetNumber } = cresult.regeocode.addressComponent;
                        const address = `${province}${city}${district}${township}${street}${streetNumber}`;
                        // 获取员工信息
                        hrApi({ staffId: this.staffCode })
                          .then(res => {
                          // 添加水印
                            this.watermark(e.url, [address, `${res.name}(${this.staffCode})`, this.$getSystemTime()]).then(cres => {
                            // 转为blob对象
                              this.fileUrl = this.dataURLtoBlob(cres.src);
                              // 手动上传
                              // this.$refs.upload.submit();
                              resolve();
                            });
                          })
                          .catch(() => {
                            reject();
                          })
                          .finally(() => {
                            mapObj.destroy();
                            document.body.removeChild(dom);
                          });
                      } else {
                        this.$message.error('根据经纬度查询地址失败');
                        mapObj.destroy();
                        document.body.removeChild(dom);
                        reject();
                      }
                    });
                  });
                } else {
                  this.$message.error(result.message);
                  mapObj.destroy();
                  document.body.removeChild(dom);
                  reject();
                }
              });
            });
          })
          .catch(e => {
            console.log(e);
            document.body.removeChild(dom);
            reject();
          });
      });
    },
    change(e) {
      console.log(e);
    },
    // 将base64转换为blob对象
    dataURLtoBlob(dataurl) {
      const arr = dataurl.split(',');
      const mime = arr[0].match(/:(.*?);/)[1];
      const bstr = window.atob(arr[1]);
      let n = bstr.length;
      const u8arr = new Uint8Array(n);
      // eslint-disable-next-line no-plusplus
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], { type: mime });
    },
  }
};
</script>

<style lang="scss" scoped>
/deep/.el-upload-list--picture-card .el-upload-list__item-actions {
  opacity: 1;
  background-color: transparent;
}

/deep/.el-upload-list--picture-card .el-upload-list__item-actions span {
  display: inline-block;
}

/deep/.el-upload-list__item-preview {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;

  .el-icon-zoom-in {
    display: none;
  }
}

/deep/.el-upload-list__item-delete {
  position: absolute;
  top: auto;
  right: 0;
  display: inline-block;
  bottom: 0px;
  width: 20px;
  background: rgba(0, 0, 0, 0.6);
}

/deep/.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete {
  position: absolute;
  font-size: 14px;
}

/deep/.el-upload--picture-card {
  width: 80px;
  height: 80px;
  line-height: 90px;
}
/deep/.el-upload-list--picture-card .el-upload-list__item {
  width: 80px;
  height: 80px;
}
/deep/.el-upload-list__item .el-icon-close-tip {
  display: none !important;
}
/deep/.el-upload-list__item {
  transition: none !important;
}
.bigImage {
  max-width: 100%;
  display: block;
  margin: auto;
}
</style>

组件中使用,需要注意点的是

  • 将upload设为不自动上传 :auto-upload="false"
  • 自定义上传 :http-request="httpRequest"
  • 在change事件回调中处理图片信息,此时去调用封装的打水印方法
  • 水印打完之后返回一个base64的图片,需要转成blob或者file对象
  • 我这里转为blob,因为转为file对象之后图片有问题,所有我就转成了blob
 // 将base64转换为blob对象
    dataURLtoBlob(dataurl) {
      const arr = dataurl.split(',');
      const mime = arr[0].match(/:(.*?);/)[1];
      const bstr = window.atob(arr[1]);
      let n = bstr.length;
      const u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], { type: mime });
    },
  • 最后再调用手动上传
// 转为blob对象
 this.fileUrl = this.dataURLtoBlob(cres.src);
// 手动上传
 this.$refs.upload.submit();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容