微信小程序 非webview分享给好友及分享海报
UI展示
点击分享显示分享sheet:
点击生成海报,展示海报预览图片:
组件目录结构:
代码
works文件
woks.json中引入: "share-sheet": "/components/common/share/share-sheet/share-sheet",
works.wxml中引入:<share-sheet show="{{showShareSheet}}" share-data="{{shareData}}" />
works.js中相关:
data: {
// ...
/*分享相关参数 */
showShareSheet: false, //是否显示分享弹窗
shareData: null, //默认为null
// shareData: {
// showImg: '', //分享图片
// title: '', //标题
// subordinateTitle: '', //次标题
// designerHeadPortrait: '', //设计师头像
// designerName: '', //设计师名字
// qrCodeImg: '', //分享二维码图片
// }, //获取分享的相关数据
},
getWorkDetail(id) {
// ...
//异步请求数据设置分享数据
this.setData({
shareData
})
/ ...
},
// 点击分享按钮
shareAction() {
this.setData({
showShareSheet: true,
});
},
该处使用的url网络请求的数据。
share-sheet文件
share-sheet组件见https://blog.youkuaiyun.com/feiyupang/article/details/125605602
poster-share文件
json文件引入:"painter":"/components/painter/painter"
wxml:
<painter palette="{{posterData}}" use2D="{{true}}" customStyle='position: absolute; left: -9999rpx;' bind:imgOK="onImgOK" bind:imgErr="onImgErr" />
<!-- use2D -->
<view class="preview-poster-container" hidden="{{ !show }}" catchtouchmove="poptouchmove">
<view class="shade" ></view>
<view class="content" style="max-height: {{maxHeight}}px">
<view class="close-bar">
<view class="close-btn" catchtap="closePreviewPop">
<image class="close-icon" src="/icons/cover-view/close-white.png"></image>
</view>
</view>
<scroll-view scroll-y class="poster-box" style="height: {{imgHeight}}px">
<image wx:if="{{tempFilePath}}" src="{{tempFilePath}}" class="poster" mode="widthFix" bindload="filePathLoaded" binderror="filePathError"></image>
</scroll-view>
<view class="save-btn" catchtap="savePoster" >保存图片</view>
</view>
</view>
<!--获取文字高度使用-->
<canvas type="2d" id="heightCanvas" style="position: absolute; left: -9999rpx;"></canvas>
js:
const app = getApp();
Component({
options: {
addGlobalClass: true,
},
/**
* 组件的属性列表
*/
properties: {
show: {
type: Boolean,
value: false
},
shareData: {
type: Object,
value: null,
observer(val) {
if (val) {
this.data.isCreating = true;
this.setPosterData();
}
},
},
// 是否在生成海报之前点击了生成海报按钮
clickedCreateBtn: {
type: Boolean,
value: false,
observer(val) {
if (val) {
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.isCreating) {
this.setPosterData();
}
}
},
},
},
lifetimes: {
attached: function () {
this.attachedMethod();
},
},
attached: function () {
this.attachedMethod();
},
/**
* 组件的初始数据
*/
data: {
// 分享相关参数
posterData: null,
tempFilePath: '', //生成的海报图片临时存储路径
imgHeight: 0, // 盛放图片的盒子高度
maxHeight: 0, // 海报预览的最大高度
isCreating: false, //是否正在生成中
},
/**
* 组件的方法列表
*/
methods: {
attachedMethod() {
let self = this;
let pages = getCurrentPages(),
currentPage = pages[pages.length - 1];
self.data.currentPage = currentPage;
app.getSystemInfo(function (systemMsg) {
// 根据实际情况调整
const maxHeight = Math.floor(
systemMsg.height -
systemMsg.navigationBarHeight -
(68 * systemMsg.screenWidth) / 750
);
/* 海报预览盒子中海报的最大高度
根据实际情况调整
*/
const maxImgHeight = Math.floor(
maxHeight - ((68 + 156) * systemMsg.screenWidth) / 750
);
self.data.screenWidth = systemMsg.screenWidth;
self.data.maxImgHeight = maxImgHeight || 0;
self.setData({
maxHeight,
});
});
},
//阻止滑动事件 防止page滑动
poptouchmove: function () {
return 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;
}
//作品详情页
if (self.data.currentPage.route === 'pages/details/work/work') {
import('../page-poster-data/work')
.then(({ createPosterData }) => {
createPosterData(self.textHeightCtx, self.data.shareData)
.then(res => {
self.setData({
posterData: res,
});
})
.catch(error => {
wx.hideLoading();
self.data.isCreating = false;
self.data.clickedCreateBtn = false;
wx.showToast({
title: (error && error.errMsg) || '生成失败,请重试',
icon: 'none',
});
});
})
.catch(error => {
console.log('createPosterData 失败', error);
wx.hideLoading();
self.data.isCreating = false;
this.data.clickedCreateBtn = false;
});
}
},
// 保存图片到本地
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();
}
},
});
},
//保存图片到相册
saveImageToPhoto: function () {
let self = this;
const tempFilePath = self.data.tempFilePath;
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success(res) {
wx.showToast({
title: '保存成功',
icon: 'success',
});
self.setData({
show: 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);
},
});
},
onImgOK(e) {
console.log('onImgOK', e);
this.data.isCreating = false;
this.data.tempFilePath = e.detail.path;
this.setData({
tempFilePath: this.data.tempFilePath,
});
this.triggerEvent('posterStatus', e.detail.path);
wx.hideLoading();
if (this.data.clickedCreateBtn) {
this.setData({
show: true,
});
this.data.clickedCreateBtn = false;
}
},
onImgErr(err) {
this.data.isCreating = false;
console.log('onImgErr', err);
wx.hideLoading();
},
closePreviewPop() {
this.setData({
show: false,
});
},
//预览海报图片加载完成
filePathLoaded(event) {
const { width, height } = event.detail;
//计算出预览海报的在页面中的展示高度
const scaleImgHeight = Math.floor((this.data.screenWidth * 0.67 * height) / width);
this.data.imgHeight =
scaleImgHeight > this.data.maxImgHeight ? this.data.maxImgHeight : scaleImgHeight;
this.setData({
imgHeight: this.data.imgHeight,
});
},
filePathError(event) {
console.log('图片加载失败error', event);
},
},
});
less样式:
.preview-poster-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1002;
.shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1;
}
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 67%;
max-height: 90%;
z-index: 2;
//关闭按钮不算在垂直居中的里面 margint-top值为关闭按钮的高度的一半
margin-top: -34rpx;
.close-bar {
text-align: right;
width: 100%;
}
.close-btn {
display: inline-block;
padding: 20rpx 30rpx;
margin-right: -30rpx;
}
.close-icon {
width: 28rpx;
height: 28rpx;
}
.poster-box {
width: 100%;
overflow: auto;
}
.poster {
width: 100%;
height: auto;
}
.save-btn {
display: block;
width: 400rpx;
height: 112rpx;
background: rgba(0, 0, 0, 0.9);
color: #fff;
text-align: center;
line-height: 111rpx;
margin: 44rpx auto 0 auto;
font-size: 32rpx;
font-weight: 500;
letter-spacing: 3rpx;
border-radius: 99rpx;
}
}
}
page-poster-data文件
work.js
import shareUtils from '../share-utils';
//获取图片的高度 750rpx对应的高度
const app = getApp();
const createPosterData = (ctx, shareData) => {
return new Promise(async (resolve, reject) => {
const { showImg, title, qrCodeImg, subordinateTitle, designerHeadPortrait, designerName } =
shareData;
const titleView = {
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);
}
titleView.css.bottom = `${334 + subordinateTitleH + 40}rpx`;
//底部padding值
const paddingBottom = 338; //从副标题往下的距离 rpx
const titleDistance = 40; //主标题到副标题之间的距离 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,
},
},
//渐变矩形 //不认 to bottom 、to top等格式
{
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: '270rpx',
width: '320rpx',
height: '2rpx',
// opacity: '0.3',
// color: '#C4C4C4', //rect 下背景色用color
color: 'rgba(196, 196, 196, 0.3)',
},
},
//设计师
{
type: 'text',
text: '设计师 ',
css: {
left: '50rpx',
bottom: '200rpx',
color: '#fff',
fontSize: '28rpx',
align: 'left',
scalable: true,
lineHeight: '40rpx',
},
},
// 设计师头像
{
type: 'image',
url: designerHeadPortrait,
css: {
bottom: '142rpx',
left: '50rpx',
width: '42rpx',
height: '42rpx',
scalable: true,
borderRadius: '4rpx',
},
},
//设计师姓名
{
type: 'text',
text: designerName,
css: {
left: '110rpx',
bottom: '142rpx',
color: '#fff',
fontSize: '28rpx',
align: 'left',
scalable: true,
lineHeight: '42rpx',
},
},
// 优秀作品 即可点击收藏
{
type: 'text',
text: '优秀作品,即刻点击收藏',
css: {
left: '50rpx',
bottom: '78rpx',
color: 'rgba(255,255,255,0.5)',
fontSize: '28rpx',
scalable: true,
lineHeight: '40rpx',
},
},
// 二维码
{
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;
share-utils js
公共share-utils js文件,计算文字高度、px/rpx简单换算等:
//获取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,
};