vue生成商品海报
前言:项目中有一个分享商品海报需求,分别为h5端和app端(其中app端)是使用uniapp来进行开发的,其中h5端是使用vue-canvas-poster插件来进行开发的,app是使用uniapp插件市场的l-painter插件进行开发的,接下来我们具体看下它们的使用方法
App端
项目原来具有分享到微信等供应商的,使用了uni.getProvider(OBJECT)方法获取供应商并且分享
如图点击右上角分享按钮下面会弹出分享的供应商(这是项目原本自带的功能),同时我的需求需要再此基础上再次弹出海报
实现步骤
1.首先在项目中导入插件l-painter,插件地址海报画板 - DCloud 插件市场大家可自行获取
2.我的是做法是把分享的海报封装成一个组件,具体代码如下
<template>
<view v-if="showShare">
<!-- 遮罩放在最外侧,遮罩层级低于分享图,高于商品详情页面 -->
<view class="maskBg"></view>
<view class="talent_poster">
<div style="position:relative;">
<div class="closeBtn" @click="showShare = false" v-show="operateIcon">X</div>
<div>
<image :src="path" mode="widthFix"></image>
</div>
<div class="download" @click="download" v-show="operateIcon">
<image style="width: 40rpx;" src="../../static/download.png" mode="widthFix"></image>
</div>
</div>
<l-painter :board="poster" isCanvasToTempFilePath @success="path = $event" hidden />
</view>
</view>
</template>
<script>
export default {
props: {
goodsInfo: {
type: Object,
default: () => {}
},
qucodeUrl:{
type: String,
default: ''
}
},
data() {
return {
path: '',
operateIcon:false,
showShare: false,
poster: {
css: {
width: '750rpx',
marginTop: '40rpx',
marginBottom: '40rpx',
background: '#fff',
borderRadius: '20px'
},
views: [
{
css: {
marginLeft: '40rpx',
marginTop: '30rpx',
padding: '26rpx',
boxSizing: 'border-box',
background: '#fff',
borderRadius: '16rpx',
width: '670rpx'
// boxShadow: '0 20rpx 58rpx rgba(0,0,0,.15)'
},
views: [
{
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
type: 'image',
css: {
objectFit: 'cover',
objectPosition: '50% 50%',
width: '606rpx',
height: '606rpx'
}
},
{
css: {
marginTop: '32rpx',
color: '#FF0000',
fontWeight: 'bold',
fontSize: '28rpx',
lineHeight: '1em'
},
views: [
{
text: '¥',
type: 'text',
css: {
verticalAlign: 'bottom'
}
},
{
text: '39',
type: 'text',
css: {
verticalAlign: 'bottom',
fontSize: '60rpx'
}
},
{
text: '¥59.99',
type: 'text',
css: {
verticalAlign: 'bottom',
paddingLeft: '10rpx',
fontWeight: 'normal',
textDecoration: 'line-through',
color: '#cacaca',
fontSize: '36rpx'
}
}
],
type: 'view'
},
{
css: {
marginTop: '32rpx',
fontSize: '26rpx',
color: '#8c5400'
},
views: [
{
text: '肖富贵卓越系列',
type: 'text',
css: {
fontSize: '28rpx',
color: '#3D3D3D'
}
}
],
type: 'view'
},
{
css: {
// marginTop: '20rpx',
fontSize: '26rpx',
color: '#8c5400'
},
views: [
{
text: '.................................................................................................',
type: 'text',
css: {
fontSize: '28rpx',
color: '#D8D8D8'
}
}
],
type: 'view'
},
{
css: {
marginTop: '20rpx'
},
views: [
{
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
type: 'image',
css: {
width: '200rpx',
height: '200rpx',
color: '#3D3D3D',
fontSize: '24rpx',
alignSelf: 'flex'
}
},
{
text: '1.保存图片到相册',
type: 'text',
css: {
paddingLeft: '20rpx',
paddingTop: '35rpx'
}
},
{
text: '2.图片分享给好友',
type: 'text',
css: {
paddingLeft: '220rpx',
position: 'absolute',
top: '40%'
}
},
{
text: '3.好友长按识别二维码可见商品详情',
type: 'text',
css: {
paddingLeft: '220rpx',
position: 'absolute',
top: '60%'
}
}
],
type: 'view'
}
],
type: 'view'
}
]
}
};
},
methods: {
open() {
this.showShare = true;
// console.log( this.qucodeUrl,'195');
// console.log( this.poster.views[0]['views'][4]['views'][0],'196*');
this.poster.views[0]['views'][0].src = this.goodsInfo.goods_img;
this.poster.views[0]['views'][1]['views'][1].text = this.goodsInfo.shop_price
this.poster.views[0]['views'][1]['views'][2].text = '¥' + this.goodsInfo.market_price
this.poster.views[0]['views'][2]['views'][0].text = this.goodsInfo.goods_name
this.poster.views[0]['views'][4]['views'][0].src = this.qucodeUrl
setTimeout(() => {
this.operateIcon = true
}, 2000);
},
download() {
uni.saveImageToPhotosAlbum({
filePath: this.path,
success() {
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail() {
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
}
}
};
</script>
<style lang="scss" scoped>
.talent_poster {
position: fixed;
width: 80%;
top: 50%;
left: 50%;
z-index: 999;
transform: translate(-50%, -50%);
margin: 0 auto;
img {
width: 100%;
height: 100%;
margin-bottom: 10px;
}
}
.maskBg {
position: fixed;
width: 100%;
height: 100vh;
background: rgba($color: #000000, $alpha: 0.3);
z-index: 998;
left: 0;
top: 0;
}
.closeBtn {
position: absolute;
right: -7px;
z-index: 99;
top: -9px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #959590;
text-align: center;
line-height: 20px;
color: white;
}
.download {
position: absolute;
bottom: 80px;
right: 0;
z-index: 1000;
}
</style>
我是使用文档中的json方法进行实现的,具体实例可参照文档
其中配置共分为四种类型:
- views:全局生成海报的最外层盒子
- text:海报需要文本使用该类型进行声明
- image:海报中使用图片使用该类型进行声明
- qrcode:海报中使用二维码需要使用该类型进行声明
其他详细的配置以及css样式具体查看官方文档进行描
3.我调用分享组件打开页面的初次效果
初版的海报就生成成功了,海报中的内容可以根据接口获取自己的数据进行替换,我的是在opne方法中尽心替换的
3.组件上的功能注意点,遮罩层的层级问题
4.遇到的问题,我们点击海报右上角的关闭按钮,需要点击两次才可以进行关闭
从图片我们可以看到全屏有一层暗灰色的遮罩,点击海报的x按钮,第一次海报没有关闭,但是下面的
供应商列表关闭了
5.问题所在:从上面的现象我们不难看出,出现问题的原因就是,供应商弹窗会带一个层级最高的遮罩在最外层,所以我们在海报上第一次点击x按钮,点击的是供应商列表的遮罩,所以会把供应商列表关闭
6.需要实现的效果:点击x关闭海报,但是不需要把供应商列表关闭掉,可能用户还需要分享到微信等其他方式也不一定
7.解决方案:由于供应商列表组件封装的遮罩是全屏的,并且点击屏幕任何地方都会将其关闭,于是我们找到遮罩层的文件,把他的遮罩高度从原来的100%,设置成0,这样就不会不经意触发遮罩进行关闭供应商列表了,在有需要关闭供应商的地方,我们使用js方法进行手动关闭即可
这样就可以解决这个问题
如下是供应商遮罩生成的代码,使用的时候调用该方法,也可以具体查看uni-share - DCloud 插件市场官方文档
function shareFn(shareInfo,drawList,callback){
//以下为计算菜单的nview绘制布局,为固定算法,使用者无关关心
let screenWidth = plus.screen.resolutionWidth
//以360px宽度屏幕为例,上下左右边距及2排按钮边距留25像素,图标宽度55像素,同行图标间的间距在360宽的屏幕是30px,但需要动态计算,以此原则计算4列图标分别的left位置
//图标下的按钮文字距离图标5像素,文字大小12像素
//底部取消按钮高度固定为44px
//TODO 未处理横屏和pad,这些情况6个图标应该一排即可
let marginTop = 25,//上间距
marginLeft=25,//左间距
iconWidth = 40,//图标宽宽
iconHeight= 40,//图标高度
icontextSpace = 15,//图标与文字间距
textHeight = 10//文字大小
let left1 = marginLeft / 360 * screenWidth;
let colNumber=Math.floor(screenWidth/(iconWidth+marginLeft));
let i=(screenWidth-(iconWidth+marginLeft)*colNumber-marginLeft)/(colNumber+1);
let initMargin=marginLeft+i;
let itemWidth=iconWidth+initMargin;
let itemHeight=iconHeight+icontextSpace+textHeight+marginTop;
let textTop=iconHeight+icontextSpace;
let alphaBg = new plus.nativeObj.View("alphaBg", { //先创建遮罩层
top: '0px',
left: '0px',
height: '0',
width: '100%',
backgroundColor: 'rgba(0,0,0,0.5)'
});
alphaBg.addEventListener("click", function() { //处理遮罩层点击
alphaBg.hide();
shareMenu.hide();
})
let shareMenu = new plus.nativeObj.View("shareMenu", { //创建底部图标菜单
bottom: '0px',
left: '0px',
height: Math.ceil(drawList.length/colNumber)*itemHeight+marginTop+44+1+'px',
width: '100%',
backgroundColor: 'rgb(255,255,255)'
});
//绘制底部图标菜单的内容
shareMenu.draw([
{
tag: 'rect',//菜单顶部的分割灰线
color: '#e7e7e7',
position: {
top: '0px',
height: '1px'
}
},
{
tag: 'font',
id: 'sharecancel',//底部取消按钮的文字
text: '取消分享',
textStyles: {
size: '14px'
},
position: {
bottom: '0px',
height: '44px'
}
},
{
tag: 'rect',//底部取消按钮的顶部边线
color: '#e7e7e7',
position: {
bottom: '45px',
height: '1px'
}
}
]);
drawList.map((v,k)=>{
let time=new Date().getTime();
let row=Math.floor(k/colNumber);
let col=k%colNumber;
let item=[{
src:v.icon,
id:Math.random()*1000+time,
tag:"img",
position:{
top:row*itemHeight+marginTop,
left:col*itemWidth+initMargin,
width:iconWidth,
height:iconWidth
}
},{
text:v.text,
id:Math.random()*1000+time,
tag:"font",
textStyles:{
size: textHeight
},
position:{
top:row*itemHeight+textTop,
left:col*itemWidth+initMargin,
width:iconWidth,
height:iconWidth
}
}];
shareMenu.draw(item);
});
shareMenu.addEventListener("click", function(e) { //处理底部图标菜单的点击事件,根据点击位置触发不同的逻辑
if (e.screenY > plus.screen.resolutionHeight - 88) { //点击了底部取消按钮
alphaBg.hide();
shareMenu.hide();
} else if (e.clientX < 5 || e.clientX > screenWidth - 5 || e.clientY < 5) {
//屏幕左右边缘5像素及菜单顶部5像素不处理点击
} else { //点击了图标按钮
let x=e.clientX;
let y=e.clientY;
let colIdx=Math.floor(x/itemWidth);
let rowIdx=Math.floor(y/itemHeight);
let tapIndex=colIdx+rowIdx*colNumber;
callback&&callback(tapIndex);
alphaBg.hide();
shareMenu.hide();
}
})
return {alphaBg,shareMenu};
};
export default shareFn;
8.最后就是根据接口动态赋值海报上的内容了
直接给json里面的文件动态赋值就行了
h5端
- 下载vue-canvas-poster
npm i vue-canvas-poster --save
2.在main.js 中引入
import VueCanvasPoster from 'vue-canvas-poster'
Vue.use(VueCanvasPoster);
3.封装成组件使用
<template>
<div v-if="shareStatus">
<div class="maskBg"></div>
<div class="talent_poster">
<!-- :widthPixels="1000" 一般不要设置最大值,如果设置了最大值,海报中图片过大,可能回出现重叠-->
<div style="height:1px;overflow:hidden">
<vue-canvas-poster
:painting="painting"
@success="canvasSuccess"
@fail="canvasFail"
></vue-canvas-poster>
</div>
<div style="position:relative;">
<div class="closeBtn" @click="shareStatus = false">X</div>
<div><img :src="posterImg" /></div>
</div>
</div>
</div>
</template>
<script>
import {
Toast
} from 'vant';
export default {
middleware: '',
props:{
goodsInfo:{
type:Object,
default:()=>{}
},
shareUrl:{
type:String,
default:''
}
},
components:{
[Toast.name]: Toast
},
data() {
return {
shareStatus:false,
posterImg: '', //生成的海报
painting: {
width: '750px',
height: '1168px',
background: '#fff',
borderRadius:'20px',
views: [
// 二维码
{
type: 'qrcode',
content: '',
background: '#000000',
css: {
color: '#000',
background: '#f5f5f5',
width: '0px',
height: '0px',
borderRadius: '10px',
borderColor: '#000000'
}
},
{
type: 'image',
url: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
text: '',
css: {
top: '60px',
right: '10%',
width: '600px',
height: '600px',
maxLines: 1,
fontSize: '70px',
color: '#0068B7'
}
},
{
type: 'text',
text: '',
css: {
top: '58%',
left: '3%',
width: '300px',
maxLines: 1,
textAlign: 'left',
fontSize: '60px',
color: '#FF0000'
}
},
{
type: 'text',
text: '',
css: {
top: '60%',
left: '40%',
width: '300px',
maxLines: 1,
textAlign: 'left',
fontSize: '36px',
color: '#CACACA',
textDecoration:'line-through'
}
},
{
type: 'text',
text: '',
css: {
top: '65%',
left: '6%',
width: '100%',
maxLines: 1,
textAlign: 'left',
fontSize: '28px',
color: '#3D3D3D'
}
},
{
type: 'text',
text: '.......................................................................................................',
css: {
top: '70%',
left: '0',
width: '100%',
maxLines: 1,
textAlign: 'center',
fontSize: '28px',
color: '#D8D8D8'
}
},
{
type: 'image',
url: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
text: '',
css: {
top: '78%',
left: '5%',
width: '200px',
height: '200px',
maxLines: 1,
fontSize: '70px',
color: '#0068B7'
}
},
{
type: 'text',
text: '1.保存图片到相册',
css: {
top: '80%',
left: '0',
width: '100%',
maxLines: 1,
textAlign: 'center',
fontSize: '24px',
color: '#3D3D3D'
}
},
{
type: 'text',
text: '2.图片分享给好友',
css: {
top: '84%',
left: '0',
width: '100%',
maxLines: 1,
textAlign: 'center',
fontSize: '24px',
color: '#3D3D3D'
}
},
{
type: 'text',
text: '3.好友长按识别二维码可见商品详情',
css: {
top: '88%',
left: '12.5%',
width: '100%',
maxLines: 1,
textAlign: 'center',
fontSize: '24px',
color: '#3D3D3D'
}
}
]
}
};
},
methods: {
canvasSuccess(src) {
this.posterImg = src;
setTimeout(() => {
if (this.posterImg !=='') {
this.open();
this.shareStatus = true;
}
}, 2000);
},
canvasFail(err) {
alert(err);
},
open() {
this.painting.views[1].url = this.goodsInfo.goods_img;
if (this.goodsInfo.shop_price.length<9) {
this.painting.views[3].css.left = '42%';
}
if (this.goodsInfo.shop_price.length<6) {
this.painting.views[3].css.left = '32%';
}
this.painting.views[2].text = '¥'+this.goodsInfo.shop_price;
this.painting.views[3].text = '¥'+this.goodsInfo.market_price;
this.painting.views[4].text = this.goodsInfo.goods_name;
this.painting.views[6].url = this.shareUrl;
// console.log(this.shareUrl,'111');
}
}
};
</script>
<style lang="scss" scoped>
.talent_poster {
position: fixed;
width: 80%;
top: 50%;
left: 50%;
z-index: 999;
transform: translate(-50%, -50%);
margin: 0 auto;
img {
width: 100%;
height: 100%;
margin-bottom: 10px;
}
}
.closeBtn {
position: absolute;
right: -7px;
z-index: 99;
top: -9px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #959590;
text-align: center;
line-height: 20px;
color: white;
}
.maskBg {
position:fixed;
width: 100%;
height: 100vh;
background: rgba($color: #000000, $alpha: 0.3);
top:0;
z-index:99
}
</style>
4.注意点
- 总体使用和uniapp的l-painter类似,其中海报内容数据都是在data中配置json数据
- 也是根据type配置类型,分别是views,qrcode,text,image
- 海报数据动态赋值也是需要修改json中默认的配置数据
5.完成效果图
更多推荐
所有评论(0)