vue+html2canvas生成寄几想要的海报

发布时间 2023-06-16 13:54:36作者: 芝麻小仙女

需求:

点击弹出一个弹窗,其中是某个作品内容的海报,需要呈现作品的内容+二维码

 

思路:

获取作品内容渲染到弹窗中,生成包含分享链接的二维码,将整个界面转为图片,用户可以长按保存,并扫描识别。

 

方案及步骤:

1.引入html2canvas实现生成图片的功能

npm install --save html2canvas

 

2.引入vue-qrcode实现生成二维码的功能

因为我用的是vue2,因此使用此插件专用与vue2的分支:npm install vue@2 @chenfengyuan/vue-qrcode@1

如果是vue3,可直接引用最新版:npm install vue@3 qrcode@1 @chenfengyuan/vue-qrcode@2

 

3.弹窗使用vant的popup组件。具体页面实现:

<!-- 分享作品预览 -->
<template>
  <van-popup
    v-model="visible"
    get-container="body"
    :style="{ backgroundColor: 'transparent', overflowY: 'visible' }"
    @opened="popupDown"
  >
    <div :class="$style.preview" @click="visible = false">
      <div id="j-canvas" :class="$style.canvas">
        <div :class="$style.popupDetail">
          <div :class="$style.detailImgInfo">
            <div :class="$style.detailImg">
              <img
                v-if="imageBase"
                :src="imageBase"
                style="width: 2.34rem; height: 2.46rem; margin-top: -0.1rem;"

              />
            </div>
            <div :class="[$style.detailInfo, $style.detailInfoFirst]">
              作者名称:{{ detailInfo.submitter }}
            </div>
            <div :class="$style.detailInfo">
              作品编号:{{ detailInfo.code_num }}
            </div>
          </div>
          <div :class="$style.detailTip"></div>
          <div :class="$style.detailTipInfo">
            <div :class="$style.detailTipInfoDiv">
              <van-field
                v-model="detailInfo.content"
                rows="5"
                autosize
                label=""
                type="textarea"
                disabled
              />
            </div>
          </div>
          <div :class="$style.footer">
            <img
              src="./images/logo.png"
              style="width: 2.06rem; height: 1.41rem"
            />
            <img
              src="./images/qrcode.png"
              style="width: 2.61rem; height: 0.92rem"
            />
            <div :class="$style.detailQrCode">
              <qrcode
                :value="`${baseURL}?code_num=${detailInfo.code_num}`"
              ></qrcode>
            </div>
          </div>
        </div>
      </div>
      <div :class="$style.screenshot"><img :src="screenUrl" /></div>
    </div>
  </van-popup>
</template>

<script>
import html2canvas from "html2canvas";

export default {
  name: "SharePreview",
  props: {
    detailInfo: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  data() {
    return {
      screenUrl: "",
      visible: false,
      baseURL: window.location.origin,
      obj: {
        imgHasLoaded: false,
        popupHasLoaded: false
      },
      imageBase: ""
    };
  },
  computed: {
    canvasParams: function() {
      const { imgHasLoaded, popupHasLoaded } = this.obj;
      return { imgHasLoaded, popupHasLoaded };
    }
  },
  watch: {
    detailInfo() {
      const { image } = this.detailInfo;
      if (image) {
        this.imageToBase64(image);
      }
    },
    canvasParams() {
      const { imgHasLoaded, popupHasLoaded } = this.canvasParams;
      // 图片生成(确保有二维码&图片后再生成图片)
      if (imgHasLoaded && popupHasLoaded) {
        this.getImg();
      }
    }
  },
  methods: {
    imgLoaded() {
      this.obj.imgHasLoaded = true;
    },
    popupDown() {
      this.obj.popupHasLoaded = true;
    },
    getImg() {
      html2canvas(document.querySelector("#j-canvas"), {
        backgroundColor: "#000"
      }).then(canvas => {
        this.screenUrl = canvas.toDataURL();
      });
    },
    async imageToBase64(src) {
      const response = await fetch(src);
      const blob = await response.blob();
      const reader = new FileReader();
      reader.onloadend = () => {
        this.imageBase = reader.result;
      };
      reader.readAsDataURL(blob);
      this.obj.imgHasLoaded = true;
    }
  },
};
</script>

<style lang="less" module>
.screenshot {
  width: 7.2rem;
  height: 10.8rem;
  position: absolute;
  top: 0.7rem;
  left: 0;
  overflow: hidden;
  opacity: 0;

  img {
    .size(100%);
  }
}
.preview {
  position: relative;

  &::before {
    .background-fill("./images/popup-close.png", 0.56rem, right 0);

    content: "";
    display: block;
    height: 0.7rem;
    pointer-events: none;
  }

  &::after {
    .size(6.71rem, 0.34rem);
    .background-fill("./images/image-preview-tip.png");

    content: "";
    display: block;
    margin: 0.3rem auto 0;
    pointer-events: none;
  }
  .canvas {
    .size(100%);
  }

  .popupDetail {
    .background-fill("./images/popup-detail-bg.png");
    width: 7.2rem;
    height: 10.8rem;
    padding-top: 0;
  }
  .detailBody {
    margin: 0 auto;
  }

  .detailImgInfo {
    width: 3.97rem;
    margin: 0 auto;
    padding-top: 0.61rem;
    .detailImg {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 3.97rem;
      height: 4.34rem;
      .background-fill("./images/img_bg.png");
    }
    .detailInfo {
      font-size: 0.24rem;
      white-space: nowrap;
      margin-bottom: 0.1rem;
      color: #ffe58f;
      padding-left: 0.5rem;
    }
    .detailInfoFirst {
      margin-top: -0.6rem;
    }
  }
  .detailTip {
    width: 2.86rem;
    height: 0.67rem;
    .background-fill("./images/title_s.png");
    margin: 0 auto 0.2rem;
  }
  .detailTipInfo {
    width: 5.88rem;
    height: 2.24rem;
    background-color: rgba(0, 0, 0, 0.2);
    border: 1px solid #ffdda3;
    border-radius: 4px;
    margin: 0 auto 0.48rem;
    color: #fff1d1;
    font-size: 0.18rem;
    line-height: 0.28rem;
    padding: 0.3rem 0.24rem;
    overflow: hidden;
    .detailTipInfoDiv {
      width: 5.4rem;
      height: 1.64rem;
      overflow: hidden;
      textarea {
        color: #fff1d1;
        -webkit-text-fill-color: #fff1d1 !important;
        overflow: hidden;
      }
    }
  }
  .footer {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .detailQrCode {
    width: 1.12rem;
    height: 1.05rem;
    .background-fill("./images/qrcode_bg.png");
    display: flex;
    justify-content: left;
    align-items: center;
    padding-left: 0.05rem;
    margin-left: 0.14rem;
    canvas {
      width: 0.92rem !important;
      height: 0.92rem !important;
    }
  }
}
</style>

  

4.其他页面使用:

    <share-preview ref="preview" :detailInfo="detailInfo" />


// 引入
import SharePreview from "@/components/Popup/SharePreview";

// 调用方法唤醒
    shareWork(item) {
      // 是否登录
      if (this.token) {
        if (item.id) {
          this.detailInfo = item;
          this.$refs.preview.visible = true;
          this.toShare();
        } else {
          this.$toast("敬请期待");
        }
      } else {
        this.$refs.login.show();
      }
    },

  

5.tips:

①因为作品数据是通过接口请求的,其中包含了作品的图片,因此要确保图片加载完毕后才生成图片,否则会因为异步的原因,在图片还没有渲染时就生成图片,保存后发现并没有作品图。尝试方案时首先使用了img的@load进行监听,但并无效果,所以换了方案,即拿到图片地址后先转为base64格式,然后赋值到页面,保证图片一定是渲染好的。

②二维码的生成也同样,因为可能会有生成时间上的差异,所以要保证二维码生成了,再去生成完整的海报图片。这里是使用了popup提供的@opened事件,监听弹窗渲染完毕打开后,再去生成图片。

③为了同时保证作品图片与二维码都生成并渲染好之后,再去生成海报,所以在data中加了一个对象obj,其中的imgHasLoaded与popupHasLoaded就是为了监听这两件事是否处理完的。在watch中监听canvasParams(也就是computed中配置的这两个值)的状态,确保两者都进行完毕后,再去执行图片生成的操作,经过测试,这个方案是可行的。

④在图片转base64格式时,可能会存在跨域的问题,需要后端同学配合处理,否则若不允许跨域,是无法成功转换格式的。

⑤生成图片的方式很直接,要生成的内容即代码中配置了id为j-canvas的div中的全部内容,可以注意到与它同级的div中放了一个图片,这个图片就是通过插件生成的海报。将这个div定位到弹窗上方,透明度调为0,这个功能就这么完成了。