用uni-app實現(xiàn)微信小程序海報分享功能
微信小程序基礎庫2.9.0以后canvas新api實現(xiàn),本文實現(xiàn)是用2.9.0新api旷祸,簡單講一下2.9.0以下用法:
效果圖:
IMG_4789.PNG
- 2.9.0以下版本創(chuàng)建
canvas
xml:
<view>
<canvas style="width: 100px; height: 100px" canvas-id="test"></canvas>
</view>
js:
// 在組件中使用canvas这难,如果是在組件中使用canvas需要在創(chuàng)建圖形上下文是加this枫甲,在頁面中使用可不加
var cxt = uni.createCanvasContext('test', this);
cxt.setFillStyle('#fd0000');
cxt.fillRect(0, 0 , 50, 50);
cxt.draw();
- 2.9.0以上版本創(chuàng)建
canvas
装黑,主要代碼片段如下:
xml
<template>
<view class="page-share-poster" @touchmove.stop="">
<view class="view-bg" :class="[bgColor]" @click="onClickBg">
<view class="view-canvas" @click.stop="">
<!-- style是設置canvas樣式的大小 -->
<canvas style="width: 100%; height: 100%;" type="2d" id="share-poster"></canvas>
</view>
</view>
</view>
</template>
js
獲取canvas
實例
// canvas實例
getCtx() {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this);
query.select('#share-poster')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 設置canvas大小
const dpr = uni.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
resolve(canvas);
})
});
},
canvas
生成圖片
// 生成圖片
canvasToTempFilePath(canvas) {
uni.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
this.shareImg = res.tempFilePath;
uni.hideLoading();
},
fail: (res) => {
uni.hideLoading();
this.$emit('onHideSharePoster');
uni.showToast({
title: '海報生成失敗',
})
}
})
},
canvas
加載網(wǎng)絡圖片之前恢恼,
加載網(wǎng)絡圖片需要先調用uni.getImageInfo把圖片下載下來民傻,生成一個臨時地址,需在微信管理平臺設置圖片域名白名單,圖片必須是https的漓踢。
// 返回的是微信存儲的臨時圖片路徑
getImageInfo(src) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: src,
success: (res) => {
resolve(res.path);
},
fail: (err) => {
uni.hideLoading();
reject(err);
}
})
})
},
canvas
加載網(wǎng)絡圖片
// canvas加載網(wǎng)絡圖片
onLoadImage(canvas, src) {
return new Promise((resolve, reject) => {
let img = canvas.createImage();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
uni.hideLoading();
reject('');
}
img.src = src;
});
},
- 基礎庫版本判斷
// 獲取當前基礎庫版本號
const { SDKVersion } = uni.getSystemInfoSync();
// 版本號比較
isFastShare = compareVersion(SDKVersion, '2.9.0') < 0 ? true : false
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[I])
const num2 = parseInt(v2[I])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
- 詳細代碼如下:
<template>
<view class="page-share-poster" @touchmove.stop="">
<view class="view-bg" :class="[bgColor]" @click="onClickBg">
<view class="view-canvas" @click.stop="">
<!-- style是設置canvas樣式的大小 -->
<canvas style="width: 100%; height: 100%;" type="2d" id="share-poster"></canvas>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
model: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
bgImage: '', // canvas背景圖
bottomImage: '', // canvas底部圖
hotImage: '', // 已搶小圖標
tagImage: '', // 驚喜價tag標簽
goodsImage: '', // 商品圖片
avatar: '', // 用戶頭像
qrCode: '', // 二維碼
shareImg: '', // 分享圖片
bgColor: '', // 背景色
}
},
mounted() {
this.fxDetail();
},
methods: {
// 點擊黑色透明背景隱藏頁面
onClickBg() {
this.$emit('onHideSharePoster');
},
// 保存圖片
onSavePoster() {
uni.saveImageToPhotosAlbum({
filePath:this.shareImg,
success: (res) => {
this.$emit('onHideSharePoster');
uni.showToast({
title: '圖片保存成功'
});
},
fail: (res) => {
uni.showToast({
title: '圖片保存失敗'
});
},
});
},
// 獲取網(wǎng)絡圖片
getImageInfo(src) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: src,
success: (res) => {
resolve(res.path);
},
fail: (err) => {
uni.hideLoading();
reject(err);
}
})
})
},
// 加載本地圖片
onLoadImage(canvas, src) {
return new Promise((resolve, reject) => {
let img = canvas.createImage();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
uni.hideLoading();
reject('');
}
img.src = src;
});
},
// canvas實例
getCtx() {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this);
query.select('#share-poster')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 設置canvas大小
const dpr = uni.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
resolve(canvas);
})
});
},
// 生成圖片
canvasToTempFilePath(canvas) {
uni.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
this.shareImg = res.tempFilePath;
uni.hideLoading();
},
fail: (res) => {
uni.hideLoading();
this.$emit('onHideSharePoster');
uni.showToast({
title: '海報生成失敗',
})
}
})
},
async fxDetail() {
uni.showLoading({
title: '海報生成中...',
});
const [bgImage, bottomImage, hotImage, tagImage, goodsImage, avatar, qrCode, canvas] = await Promise.all([
this.getImageInfo('https://img1.iqianggou.com/miniapp/poster_bg.TIeCtE.png').catch(e => e),
this.getImageInfo('https://img1.iqianggou.com/miniapp/poster_bottom.3C8Awt.png').catch(e => e),
this.getImageInfo('https://img1.iqianggou.com/miniapp/huo.Jb1aAH.png').catch(e => e),
this.getImageInfo('https://img1.iqianggou.com/miniapp/qian.8t3Rt5.png').catch(e => e),
this.getImageInfo(this.model.image).catch(e => e),
this.getImageInfo(this.userInfo.avatar).catch(e => e),
this.getImageInfo(this.model.qrimg_url).catch(e => e),
this.getCtx().catch(e => e),
]);
this.bgImage = bgImage;
this.bottomImage = bottomImage;
this.hotImage = hotImage;
this.tagImage = tagImage;
this.goodsImage = goodsImage;
this.avatar = avatar;
this.qrCode = qrCode;
this.drawFxDetialCanvas(canvas);
},
// 開始繪圖
async drawFxDetialCanvas(canvas) {
// 獲取canvas的繪圖上下文
const ctx = canvas.getContext('2d');
const [bgImg, goodsImg, bottomImg, avatar, qrCode, hotImage, tagImage] = await Promise.all([
this.onLoadImage(canvas, this.bgImage).catch(e => e),
this.onLoadImage(canvas, this.goodsImage).catch(e => e),
this.onLoadImage(canvas, this.bottomImage).catch(e => e),
this.onLoadImage(canvas, this.userInfo.avatar).catch(e => e),
this.onLoadImage(canvas, this.qrCode).catch(e => e),
this.onLoadImage(canvas, this.hotImage).catch(e => e),
this.onLoadImage(canvas, this.tagImage).catch(e => e),
]);
// 背景圖
ctx.drawImage(bgImg, 0, 0, 255, 434);
const cardWidth = 225
// 商品區(qū)域背景色
ctx.save();
this.roundRect(ctx, 15, 15, cardWidth, 300, 10, '#FFFFFF', '#FFFFFF');
ctx.restore();
// 商品圖片
ctx.save();
this.roundRect(ctx, 15, 15, cardWidth, cardWidth, 10, '', '', true);
ctx.drawImage(goodsImg, 15, 15, cardWidth, cardWidth);
ctx.restore();
// 商品名字
ctx.textAlign = 'left';
ctx.font = 'normal bold 11px sans-serif';
ctx.fillStyle = '#333333';
this.wordsWrap(ctx, this.model.name, 215, 20, 28 + cardWidth, 16, 2);
// 價格
// measureText計算文字寬高牵署,要在font之后
ctx.font = 'normal normal 11px sans-serif';
ctx.fillStyle = '#FF6D00';
ctx.fillText('¥', 20, cardWidth + 69, 12);
ctx.font = 'normal normal 22px sans-serif';
const priceWidth = Math.min(ctx.measureText(this.model.sale_price).width, 100);
ctx.fillStyle = '#FF6D00';
ctx.fillText(this.model.sale_price, 32, cardWidth + 70, priceWidth);
// 驚喜價標簽
ctx.drawImage(tagImage, priceWidth + 42, cardWidth + 57, 46, 14);
ctx.font = 'normal normal 9px sans-serif';
ctx.fillStyle = '#FFFFFF';
ctx.fillText('驚喜價', priceWidth + 54, cardWidth + 67, 40);
// 正常價
ctx.font = 'normal normal 8px sans-serif';
ctx.fillStyle = '#999999';
ctx.fillText(this.model.market_price, 20, cardWidth + 83, 200);
// 刪除線
ctx.beginPath();
ctx.moveTo(20, cardWidth + 80);
ctx.lineTo(ctx.measureText(this.model.market_price).width + 20, cardWidth + 80);
ctx.strokeStyle = '#999999';
ctx.lineWidth = 1;
ctx.stroke();
// 已搶x份
ctx.font = 'normal normal 7px sans-serif';
const salesSize = ctx.measureText(this.model.sales_volume);
const maxSaleWidth = Math.min(salesSize.width, 100);
const salesBgMinX = 235 - (maxSaleWidth + 30);
// 背景色
ctx.save();
this.roundRect(ctx, salesBgMinX, cardWidth + 70, maxSaleWidth + 25, 14, 7, '#FFF6EA', 'transparent');
ctx.restore();
// 小圖標
ctx.drawImage(hotImage, salesBgMinX + 6, cardWidth + 71, 9, 12);
// 文字
ctx.fillStyle = '#333333';
ctx.fillText(this.model.sales_volume, salesBgMinX + 18, cardWidth + 80, maxSaleWidth);
// 底部背景
ctx.drawImage(bottomImg, 0, 327, 255, 107);
// 微信頭像
ctx.save();
ctx.beginPath();
ctx.arc(47, 353, 20, 0, 2 * Math.PI);
ctx.strokeStyle = '#FFFFFF';
ctx.stroke();
ctx.clip();
ctx.drawImage(avatar, 27, 333, 40, 40);
ctx.restore();
// 昵稱
ctx.font = 'normal normal 9px sans-serif';
const userNameWidth = Math.min(ctx.measureText(this.userInfo.username).width, 70);
ctx.fillStyle = '#333333';
this.wordsWrap(ctx, this.userInfo.username, 70, 30, 388, 16, 1);
// 描述
ctx.font = 'normal normal 9px sans-serif';
ctx.fillStyle = '#666666';
ctx.fillText('發(fā)現(xiàn)一家好店', 40 + userNameWidth, 388, 60);
// 二維碼
ctx.save();
this.roundRect(ctx, 173, 358, 68, 68, 2, 'transparent', '#E1E1E1');
ctx.restore();
ctx.drawImage(qrCode, 177, 362, 60, 60);
// 生成圖片
this.canvasToTempFilePath(canvas);
},
// 畫圓角 ctx、x起點喧半、y起點奴迅、w寬度、y高度挺据、r圓角半徑取具、fillColor填充顏色、strokeColor邊框顏色
roundRect(ctx, x, y, w, h, r, fillColor, strokeColor, isTop) {
// 開始繪制
ctx.beginPath()
// 繪制左上角圓弧 Math.PI = 180度
// 圓心x起點吴菠、圓心y起點者填、半徑、以3點鐘方向順時針旋轉后確認的起始弧度做葵、以3點鐘方向順時針旋轉后確認的終止弧度
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
// 繪制border-top
// 移動起點位置 x終點占哟、y終點
ctx.moveTo(x + r, y)
// 畫一條線 x終點、y終點
ctx.lineTo(x + w - r, y)
// self.data.ctx.lineTo(x + w, y + r)
// 繪制右上角圓弧
ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
// 繪制border-right
ctx.lineTo(x + w, y + h - r)
// 繪制右下角圓弧
const newR = isTop ? 0 : r;
ctx.arc(x + w - newR, y + h - newR, newR, 0, Math.PI * 0.5)
// 繪制border-bottom
ctx.lineTo(x + r, y + h)
// 繪制左下角圓弧
ctx.arc(x + newR, y + h - newR, newR, Math.PI * 0.5, Math.PI)
// 繪制border-left
ctx.lineTo(x, y + r)
if (fillColor) {
// 因為邊緣描邊存在鋸齒酿矢,最好指定使用 transparent 填充
ctx.fillStyle = fillColor
// 對繪畫區(qū)域填充
ctx.fill()
}
if (strokeColor) {
// 因為邊緣描邊存在鋸齒榨乎,最好指定使用 transparent 填充
ctx.strokeStyle = strokeColor
// 畫出當前路徑的邊框
ctx.stroke()
}
// 剪切,剪切之后的繪畫繪制剪切區(qū)域內進行瘫筐,需要save與restore
ctx.clip()
},
// 文字換行處理
// canvas 標題超出換行處理
wordsWrap(ctx, name, maxWidth, startX, srartY, wordsHight, maxLine) {
let lineWidth = 0;
let lastSubStrIndex = 0;
let arr = [];
for (let i = 0; i < name.length; i++) {
lineWidth += ctx.measureText(name[i]).width;
if (lineWidth > maxWidth) {
arr.push({text: name.substring(lastSubStrIndex, i), startX, srartY});
srartY += wordsHight;
lineWidth = 0;
lastSubStrIndex = I;
} else if (i == name.length - 1) {
arr.push({text: name.substring(lastSubStrIndex, i + 1), startX, srartY});
}
}
let textArr = arr.slice(0, maxLine);
if (arr.length > maxLine) {
let text = textArr[maxLine - 1]['text'];
textArr[maxLine - 1]['text'] = text.substring(0, text.length - 2) + '...';
}
for (let item of textArr) {
ctx.fillText(item.text, item.startX, item.srartY);
}
},
},
}
</script>
<style lang="scss" scoped>
.page-share-poster {
.bg-white {
background-color: #FFFFFF;
}
.bg-black {
background-color: rgba(0, 0, 0, 0.5);
}
.view-bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.view-canvas {
position: absolute;
left: 50%;
bottom: 60%;
// 這邊寬高不能用rpx蜜暑, 不然會影響canvas的樣式,要跟canvas(px)的單位保持一致
width: 255px;
height: 434px;
margin-left: -127.5px;
margin-bottom: -217px;
}
}
</style>