vue+html2canvas生成寄几想要的海报
需求:
点击弹出一个弹窗,其中是某个作品内容的海报,需要呈现作品的内容+二维码
思路:
获取作品内容渲染到弹窗中,生成包含分享链接的二维码,将整个界面转为图片,用户可以长按保存,并扫描识别。
方案及步骤:
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,这个功能就这么完成了。

浙公网安备 33010602011771号