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