因为要在webview下生成海报,需要使用cover-view,根据接口返回数据动态更新海报内容,微信小程序生成海报组件有wxa-plugin-canvas、painter等,这里我们使用painter来生成海报图片,ui如下图:
该分享海报动态数据包括背景banner图、标题、副标题、设计师名字及二维码
分享弹窗及按钮UI及代码请见:webview页面分享弹窗
wxml代码如下:
>
保存图片
js代码如下
// pages/details/introduction/introduction.js
import sharePosterObj from '../../../components/common/share/page-poster-data/introduction';
const app = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
/*分享相关参数 */
showShareSheet: false, //是否显示分享弹窗
shareData: null, //默认为null
showPosterPreview: false, //是否展示海报预览弹窗
posterData: null, //生成海报数据树
tempFilePath: '',
maxHeight: 525, // 海报预览的最大高度
imgHeight: '', //海报的高度
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
let self = this;
wx.showLoading({
title: '加载中',
mask: true,
});
//获取动态数据,根据实际情况调整
self.getBaseRequestData();
app.getSystemInfo(function (systemMsg) {
self.data.screenWidth = systemMsg.screenWidth;
// 海报预览盒子最大高度 根据实际情况调整
const maxHeight = Math.floor(
systemMsg.availableHeight - (68 * systemMsg.screenWidth) / 750
);
self.setData({
maxHeight,
});
});
},
// 请求接口、获取动态数据
getBaseRequestData() {
const self = this;
baseUtil.baseConfigData().then(data => {
self.data.shareData = self.data.shareData || {
showImg: '',
title: '',
subordinateTitle: '',
qrCodeImg: '',
posterDescribe: '',
};
// 在这里对shareData 重新赋值并设置setData
// ... 代码省略 根据具体情况设置
// 给painter组件设置参数
self.setPosterData();
});
},
/********分享相关开始********/
// 点击分享按钮
shareAction() {
this.setData({
showShareSheet: true,
});
},
poptouchmove: function () {
return false;
},
//点击取消按钮
quickAction() {
this.data.showShareSheet = false;
this.setData({
showShareSheet: false,
});
},
/********生成海报相关开始********/
//获取ctx
getTextHeightCtx() {
const self = this;
return new Promise(resolve => {
const query = wx.createSelectorQuery().in(self);
query
.select('#heightCanvas')
.fields({ node: true, size: true })
.exec(res => {
const canvasNode = res[0].node;
const ctx = canvasNode.getContext('2d');
resolve(ctx);
});
});
},
async setPosterData() {
const self = this;
if (!self.textHeightCtx) {
const textHeightCtx = await self.getTextHeightCtx();
self.textHeightCtx = textHeightCtx;
}
sharePosterObj
.createPosterData(self.textHeightCtx, self.data.shareData)
.then(res => {
self.setData({
posterData: res,
});
})
.catch(error => {
wx.hideLoading();
wx.showToast({
title: (error && error.errMsg) || '生成失败,请重试',
icon: 'none',
});
});
},
// 保存图片到本地
savePoster() {
this.isWritePhotosAlbum();
},
//判断是否授权(访问相册)
isWritePhotosAlbum: function () {
let self = this;
//判断是否授权
wx.getSetting({
success: function (res) {
let writePhotosAlbum = res.authSetting['scope.writePhotosAlbum'];
if (writePhotosAlbum) {
//已授权 //true
self.saveImageToPhoto();
} else if (writePhotosAlbum != undefined && writePhotosAlbum != true) {
//拒绝授权了 //false
self.data.savePhotoIng = false;
wx.showModal({
title: '',
content: '您还未授权保存图片到相册,请确认授权',
showCancel: true,
cancelText: '取消',
confirmText: '确认',
success: function (e) {
//点了查看规则
if (e.confirm) {
//针对用户保存图片的时候可能会拒绝授权,再次点击时需要调起授权窗口
self.openSetting();
} else {
//取消
}
},
});
} else {
//第一次授权 //undefined
self.saveImageToPhoto();
}
},
});
},
//授权操作(访问相册)
openSetting: function () {
let self = this;
//调起授权弹窗
wx.openSetting({
success(res) {
//同意授权
if (res.authSetting['scope.writePhotosAlbum']) {
self.saveImageToPhoto();
}
},
});
},
toPx(rpx, int) {
if (int) {
return parseInt(rpx * this.factor * this.pixelRatio);
}
return rpx * this.factor * this.pixelRatio;
},
toRpx(px, int) {
if (int) {
return parseInt(px / this.factor);
}
return px / this.factor;
},
//点击生成海报按钮
createPosterAction() {
if (!this.data.tempFilePath) {
this.setData({
clickCreatePosterBtn: true,
showPosterPreview: false,
showShareSheet: false,
});
if (!this.data.shareData) {
// this.data.isCreating = false;
// this.data.clickedCreateBtn = false;
wx.showToast({
title: '分享数据不存在',
icon: 'none',
});
return;
}
wx.showLoading({
title: '生成中...',
mask: true,
});
if (!this.data.posterData) {
this.setPosterData();
}
return;
}
this.setData({
showPosterPreview: true,
clickCreatePosterBtn: false,
showShareSheet: false,
});
},
onImgOK(e) {
console.log('onImgOK', e);
const self = this;
self.data.tempFilePath = e.detail.path;
//获取海报图高度
wx.getImageInfo({
src: e.detail.path,
success(res) {
const { width, height } = res;
let scaleHeight = Math.floor((self.data.screenWidth * 0.67 * height) / width);
if (scaleHeight > self.data.maxHeight) {
scaleHeight = self.data.maxHeight;
}
self.setData({
imgHeight: scaleHeight,
});
},
fail() {},
});
self.setData({
tempFilePath: self.data.tempFilePath,
});
wx.hideLoading();
if (self.data.clickCreatePosterBtn) {
self.setData({
showPosterPreview: true,
});
self.data.clickCreatePosterBtn = false;
}
},
onImgErr(err) {
console.log('onImgErr', err);
wx.hideLoading();
},
closePreviewPop() {
this.setData({
showPosterPreview: false,
});
},
//保存图片到相册
saveImageToPhoto: function () {
let self = this;
const tempFilePath = self.data.tempFilePath;
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success(res) {
wx.showToast({
title: '保存成功',
icon: 'success',
});
self.setData({
showPosterPreview: false,
});
},
fail: function (res) {
if (res.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
toast('您拒绝了授权无法保存图片 ');
} else {
if (
res.errMsg === 'saveImageToPhotosAlbum:fail:auth canceled' ||
res.errMsg === 'saveImageToPhotosAlbum:fail cancel'
) {
console.log('用户取消');
} else {
wx.showToast({
title: '保存失败,请重试',
icon: 'none',
});
}
}
},
complete: function (res) {
console.log('保存图片到本地 结束', res);
},
});
},
});
less样式如下:
其中引用的两个样式可以参考webview页面分享弹窗
@import '../../../components/common/share/share-sheet/share-sheet.wxss';
@import '../../../components/common/share/poster-share/poster-share.wxss';
.bottom-fixed-bar {
position: fixed;
box-sizing: content-box;
bottom: 0;
left: 0;
width: 100%;
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
background-color: #fff;
.icon {
margin-right: 16rpx;
width: 32rpx;
height: 32rpx;
}
.bar-info {
width: 100%;
height: 112rpx;
padding: 0 48rpx;
background-color: #fff;
}
.share-btn {
position: relative;
margin-right: 24rpx;
width: 214rpx;
height: 80rpx;
border-radius: 40rpx;
background: #ffffff;
border: 2rpx solid #222222;
text-align: center;
line-height: 79rpx;
font-size: 30rpx;
}
.share-main-btn {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
width: 214rpx;
height: 80rpx;
line-height: 79rpx;
color: rgba(0, 0, 0, 0);
&::after {
display: none;
}
}
.collect-btn {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 80rpx;
line-height: 79rpx;
border-radius: 40rpx;
background: #0c0c0c;
color: #fff;
font-size: 30rpx;
}
}
生成painter json数据树的js “page-poster-data/introduction“:
//作品详情页面的postr data 数据
import shareUtils from '../share-utils';
const app = getApp();
const createPosterData = (ctx, shareData) => {
return new Promise(async (resolve, reject) => {
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo');
const { showImg, title, qrCodeImg, subordinateTitle, posterDescribe } = shareData;
const titleView = {
// id: 'title',
type: 'text',
text: title,
css: {
left: '48rpx',
width: '450rpx',
fontWeight: '500',
color: '#fff',
fontSize: '50rpx',
lineHeight: '72rpx',
scalable: true,
maxLines: 2,
},
};
const subordinateTitleView = {
type: 'text',
text: subordinateTitle,
css: {
left: '48rpx',
bottom: '334rpx',
width: '450rpx',
color: '#fff',
fontSize: '26rpx',
lineHeight: '40rpx',
scalable: true,
maxLines: 2,
},
};
let scaleImgHeight = 0;
try {
const imgPositionData = await shareUtils.getImgPositionData(showImg);
if (imgPositionData) {
scaleImgHeight = imgPositionData.scaleHeight;
}
} catch (error) {
reject(error);
return;
}
let titleH = 0;
const titlePositionData = await shareUtils.getTextPositionData({ ctx, view: titleView });
if (titlePositionData) {
titleH = shareUtils.toRpx(titlePositionData.height);
}
let subordinateTitleH = 0;
const subordinateTitlePositionData = await shareUtils.getTextPositionData({
ctx,
view: subordinateTitleView,
});
if (subordinateTitlePositionData) {
subordinateTitleH = shareUtils.toRpx(subordinateTitlePositionData.height);
}
const paddingBottom = 380; //从副标题往下的距离 rpx
const titleDistance = 40; //主标题到副标题之间的距离 rpx
titleView.css.bottom = `${paddingBottom + subordinateTitleH + titleDistance}rpx`;
const posterHeight = Math.ceil(
scaleImgHeight + (titleH * 2) / 3 + titleDistance + subordinateTitleH + paddingBottom
);
const distanceTop = Math.ceil(scaleImgHeight * 0.38); // 渐变色距离顶部的距离 rpx
//渐变背景的高度
const linearGradientHeight = Math.ceil(
distanceTop + (titleH * 2) / 3 + titleDistance + subordinateTitleH + paddingBottom
);
const startLinear =
Math.ceil((1 - distanceTop / linearGradientHeight).toFixed(2) * 100) + '%';
// 宽度和高度必须 位置必须rpx才能生效,否则默认左上角 0 0 位置 渐变后面的百分数必须写
const data = {
width: '750rpx',
height: `${posterHeight}rpx`,
background: '#060419',
views: [
// 顶栏图片
{
type: 'image',
url: showImg,
css: {
top: '0rpx',
left: '0rpx',
width: '750rpx',
mode: 'widthFix',
scalable: true,
},
},
//渐变矩形
{
type: 'rect',
css: {
left: '0rpx',
bottom: '0rpx',
width: '750rpx',
height: `${linearGradientHeight}rpx`,
//从下向上的渐变
color: `linear-gradient(180deg, rgba(6, 4, 25, 1) 0%, rgba(7,4,26,1) ${startLinear}, transparent 100%)`,
scalable: true,
},
},
//标题
titleView,
// 副标题
subordinateTitleView,
//分割线
{
type: 'rect',
css: {
left: '50rpx',
bottom: '250rpx',
width: '320rpx',
height: '2rpx',
color: 'rgba(196, 196, 196, 0.3)',
},
},
// 个人头像
{
type: 'image',
url: (userInfo && userInfo.headPortraitUrl) || '',
css: {
bottom: '142rpx',
left: '50rpx',
width: '42rpx',
height: '42rpx',
scalable: true,
borderRadius: '4rpx',
},
},
//个人昵称
{
type: 'text',
text: (userInfo && userInfo.nickName) || '',
css: {
left: '110rpx',
bottom: '142rpx',
color: '#fff',
fontSize: '28rpx',
align: 'left',
scalable: true,
lineHeight: '42rpx',
},
},
// 邀请您一起做客直播间
{
type: 'text',
text: posterDescribe,
css: {
left: '50rpx',
bottom: '78rpx',
width: '440rpx',
color: 'rgba(255,255,255,0.5)',
fontSize: '28rpx',
lineHeight: '30rpx',
scalable: true,
maxLines: 1,
},
},
// 二维码
{
type: 'image',
// url: codeImg,
url: qrCodeImg,
css: {
bottom: `50rpx`,
right: '48rpx',
backgroundColor: '#fff',
width: '160rpx',
height: '160rpx',
scalable: true,
borderRadius: '160rpx',
},
},
],
};
resolve(data);
});
};
module.exports.createPosterData = createPosterData;
引用计算文本高度的组件 js “share-utils“ :
//获取text类型的行高 行数等
const screenWidth = wx.getSystemInfoSync().screenWidth;
/*
text:文案
fontSize:字体大小 只支持rpx 数值类型
maxLines: 文案最大行数 可不传 数值类型
width: 文案宽度 可不传 只支持rpx 数值类型
padding: 文案padding值为数组格式 只支持rpx值 可不传
*/
const getTextPositionData = ({ ctx, view }) => {
let paddings = doPaddings(view.css.padding);
const textArray = String(view.text).split('\n');
// 处理多个连续的'\n'
for (let i = 0; i < textArray.length; ++i) {
if (textArray[i] === '') {
textArray[i] = ' ';
}
}
if (!view.css.fontSize) {
view.css.fontSize = '20rpx';
}
const fontWeight = view.css.fontWeight || '400';
const textStyle = view.css.textStyle || 'normal';
ctx.font = `${textStyle} ${fontWeight} ${toPx(view.css.fontSize)}px "${
view.css.fontFamily || 'sans-serif'
}"`;
// 计算行数
let lines = 0;
// let totalWidth = 0
const linesArray = [];
for (let i = 0; i < textArray.length; ++i) {
const textLength = ctx.measureText(textArray[i]).width;
const minWidth = toPx(view.css.fontSize) + paddings[1] + paddings[3];
let partWidth = view.css.width
? toPx(view.css.width) - paddings[1] - paddings[3]
: textLength;
if (partWidth < minWidth) {
partWidth = minWidth;
}
const calLines = Math.ceil(textLength / partWidth);
// 取最长的作为 width
// totalWidth = partWidth > totalWidth ? partWidth : totalWidth;
lines += calLines;
linesArray[i] = calLines;
}
lines = view.css.maxLines < lines ? view.css.maxLines : lines;
const lineHeight = view.css.lineHeight ? toPx(view.css.lineHeight) : toPx(view.css.fontSize);
const height = lineHeight * lines;
return {
lines, //行数
height, //高度
};
};
//计算rpx px转换用
const toPx = (rpx, int, factor = screenWidth / 750, pixelRatio = 1) => {
rpx = rpx.replace('rpx', '');
if (int) {
return parseInt(rpx * factor * pixelRatio);
}
return rpx * factor * pixelRatio;
};
const toRpx = (px, int, factor = screenWidth / 750) => {
if (int) {
return parseInt(px / factor);
}
return px / factor;
};
const doPaddings = padding => {
let pd = [0, 0, 0, 0];
if (padding) {
const pdg = padding.split(/\s+/);
if (pdg.length === 1) {
const x = toPx(pdg[0]);
pd = [x, x, x, x];
}
if (pdg.length === 2) {
const x = toPx(pdg[0]);
const y = toPx(pdg[1]);
pd = [x, y, x, y];
}
if (pdg.length === 3) {
const x = toPx(pdg[0]);
const y = toPx(pdg[1]);
const z = toPx(pdg[2]);
pd = [x, y, z, y];
}
if (pdg.length === 4) {
const x = toPx(pdg[0]);
const y = toPx(pdg[1]);
const z = toPx(pdg[2]);
const a = toPx(pdg[3]);
pd = [x, y, z, a];
}
}
return pd;
};
// 获取图片宽高以及750像素下的高度
export const getImgPositionData = img => {
return new Promise((resolve, reject) => {
if (img) {
wx.getImageInfo({
src: img,
success(res) {
const { width, height } = res;
const scaleHeight = Math.ceil((750 * height) / width);
resolve({
width,
height,
scaleHeight,
});
},
fail() {
reject({
errMsg: '获取图片信息失败, 请重试',
});
},
});
} else {
// wx.hideLoading();
reject({
errMsg: '图片不存在',
});
}
});
};
export default {
getTextPositionData,
getImgPositionData,
toPx,
toRpx,
};
提示:以下均为真机测试,实际情况可能会随着时间改变,请注意自测