🌟 组件亮点功能
- 双模式弹窗:支持微信分享与海报生成两种交互
- 智能参数解析:自动处理扫码场景参数
- 自适应画布:完美适配不同设备屏幕
- 安全保存:完善的相册权限处理流程
🛠 核心实现解析
1. 参数传递流程图
graph TD
A[页面调用] --> B{参数类型}
B -->|scene参数| C[后端解析]
B -->|直接code| D[直接使用]
C --> E[获取详细数据]
D --> F[数据展示]
2. Canvas绘制优化点
// 使用Promise保证图片加载顺序
Promise.all([img1, img2]).then(res => {
// 动态计算画布尺寸
const ratio = canvasWidth / originWidth;
canvasHeight = originHeight * ratio + codeSize;
// 添加圆角矩形背景
ctx.arc(x+r, y+r, r, Math.PI, Math.PI*1.5);
// ...其他路径绘制
ctx.clip();
// 层级绘制策略
ctx.drawImage(背景图, 0, 0, ...);
ctx.drawImage(二维码, 居中位置计算, ...);
});
3. 安全保存策略
uni.saveImageToPhotosAlbum({
success: () => {
// 成功提示
},
fail: (err) => {
// 引导开启权限
uni.openSetting({
success: (res) => {
// 二次授权处理
}
});
}
});
🚀 性能优化建议
- 图片预加载
在组件挂载时预加载静态资源:
mounted() {
this.shareList.forEach(item => {
uni.preloadImage(item.icon);
});
}
- Canvas缓存策略
对已生成的canvas内容进行本地缓存:
const cacheKey = `poster_${Date.now()}`;
try {
uni.setStorageSync(cacheKey, tempFilePath);
} catch (e) {
console.error('存储失败');
}
- 动态分辨率适配
根据DPI调整绘制精度:
const dpi = uni.getSystemInfoSync().pixelRatio;
ctx.scale(dpi, dpi);
🐞 常见问题排查
Q1:海报模糊怎么办?
✅ 解决方案:
- 确保使用2倍图资源
- 设置canvas尺寸时乘以pixelRatio
const dpi = uni.getSystemInfoSync().pixelRatio;
canvasWidth *= dpi;
canvasHeight *= dpi;
Q2:安卓保存失败?
✅ 处理方案:
// 增加格式转换
wx.canvasToTempFilePath({
fileType: 'jpg',
quality: 0.8,
// ...
});
Q3:扫码进入无数据?
✅ 排查步骤:
- 检查scene参数是否URL编码
- 验证后端接口返回数据结构
- 添加失败重试机制:
let retryCount = 0;
const getData = () => {
appModel.analysisCode(params).catch(() => {
if(retryCount++ < 3) getData();
});
}
📦 组件注册建议
推荐使用按需加载方式:
// 在需要的页面中
import Poster from '@/components/poster';
export default {
components: {
Poster
}
}
完整代码
<template>
<view class="poster">
<u-popup v-model="shareShow" mode="bottom" border-radius="20" height="420rpx" @close="shareShowFn">
<view class="shareBox">
<button class="shareButton" open-type="share" @click="shareLink">
<u-icon :label="shareList[0].label" label-pos="bottom" margin-top="20" label-color="#282828"
size="110" :name="shareList[0].icon"></u-icon>
</button>
<u-icon @click="posterShowFn" style="width: 50%;justify-content: center;" :label="shareList[1].label"
label-pos="bottom" margin-top="20" label-color="#282828" size="110" :name="shareList[1].icon">
</u-icon>
</view>
<view class="shareCancel" @click="$emit('posterClose',1)">取消</view>
</u-popup>
<u-popup v-model="posterShow" mode="center" width="85%" safe-area-inset-bottom @close="$emit('posterClose',1)">
<view class="canvasBox">
<!-- <u-image class="imageBox" border-radius="14" :show-error="false" :src="posterImg" mode="widthFix"
:fade="false"></u-image> -->
<canvas canvas-id="shareCanvas" class="canvas"
:style="{height: canvasHeight+'px',width:canvasWidth+'px'}">
</canvas>
</view>
<u-button class="button" v-show="drawFinish" hover-class="none" type="primary" @click="downImg"
shape="circle">保存图片
</u-button>
</u-popup>
</view>
</template>
<script>
import {
AppModel
} from '@/models/app.js'
const appModel = new AppModel();
export default {
data() {
return {
shareShow: true,
posterShow: false,
posterImg: 'https://www.xxxx.com/home/photo/hyq/wechat/weChatShare.png',
shareList: [{
label: '微信好友',
icon: 'https://www.xxxxxx.com/home/photo/hyq/wechat/share_wx.png'
}, {
label: '生成海报',
icon: 'https://www.xxxxxx.com/home/photo/hyq/wechat/share_hb.png'
}],
pageData: {
post_img: '',
codePng: 'https://www.xxxxxx.com/home/photo/hyq/wechat/qrcodetest.png'
},
ctx: null,
windowObj: {},
canvasHeight: 0,
canvasWidth: 0,
drawFinish: false
}
},
props: {
posterData: {
type: Object,
default: {}
}
},
onReady() {
// 在自定义组件下,第二个参数传入组件实例this
this.ctx = wx.createCanvasContext('shareCanvas', this)
},
methods: {
posterShowFn() {
this.posterShow = true
this.shareShow = false
appModel.shareGetCode(this.posterData).then(res => {
console.log(res, 'shareGetCode');
this.exportPost(res.data)
})
},
shareShowFn() {
if (!this.posterShow) {
this.$emit('posterClose', 1)
}
},
//获取图片的基本信息,即将网络图片转成本地图片,
getImageInfo(src) {
console.log(src, "src,src");
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: (res) => {
console.log(res)
resolve(res)
},
fail: (res) => {
uni.hideLoading()
uni.showToast({
title: '图片下载失败',
icon: 'none',
duration: 3500
})
reject(res)
}
})
});
},
downImg() {
let that = this
uni.showLoading({
title: '海报下载中',
mask: true
})
//画布。2.9.0 起支持一套新 Canvas 2D 接口(需指定 type 属性)
wx.canvasToTempFilePath({
quality: 1,
canvasId: 'shareCanvas',
// width: that.canvasWidth,
// height: that.canvasHeight,
// destWidth: that.canvasWidth*2,
// destHeight: that.canvasHeight*2,
fileType: 'png', //设置导出图片的后缀名
success: function(res) {
that.pageData.post_img = res.tempFilePath
//保存图片到本地
uni.saveImageToPhotosAlbum({
filePath: that.pageData.post_img,
success: function() {
uni.hideLoading()
uni.showToast({
title: '保存成功'
})
that.$emit('posterClose', 1)
},
fail: (err) => {
uni.hideLoading()
uni.showModal({
title: '提示',
content: '需要您授权保存相册',
showCancel: false,
success(res) {
if (res.confirm) {
uni.openSetting({
success(settingdata) {
if (settingdata.authSetting[
'scope.writePhotosAlbum'
]) {
uni.showModal({
title: '提示',
content: '获取权限成功,再次保存图片即可成功',
showCancel: false,
})
} else {
uni.showModal({
title: '提示',
content: '获取权限失败,无法保存到相册',
showCancel: false
})
}
}
})
}
}
})
}
})
},
fail: (err) => {
uni.hideLoading()
console.log(err)
}
}, this)
//uni.canvasToTempFilePath(object, component)在自定义组件下,第二个参数传入自定义组件实例,以操作组件内 <canvas> 组件
},
exportPost(codeData) {
let that = this
uni.showLoading({
title: '海报生成中',
mask: true
})
uni.getSystemInfo({
success: (systemInfo) => {
console.log(systemInfo, 'systemInfo')
//获取系统的基本信息,为后期的画布和底图适配宽高
Promise.all([that.getImageInfo(codeData.posterPath), that.getImageInfo(codeData
.qrCodePath)]).then(res => {
//获取底图和二维码图片的基本信息,通常前端导出的二维码是base64格式的,所以要转成图片格式的才可以获取图片的基本信息
that.canvasWidth = systemInfo.windowWidth * 0.85 //设置画布的宽高
let ratio = that.canvasWidth / res[0].width
let codeWidth = res[1].width * 0.27
that.canvasHeight = res[0].height * ratio + codeWidth + 20
// 设置背景色
let x = 0,
y = 0,
w = that.canvasWidth,
h = that.canvasHeight,
r = 10
// that.ctx.beginPath()
// // 因为边缘描边存在锯齿,最好指定使用 transparent 填充
// // that.ctx.setFillStyle('transparent')
// that.ctx.setStrokeStyle('transparent')
// // 左上角
// that.ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
// // border-top
// that.ctx.moveTo(x + r, y)
// that.ctx.lineTo(x + w - r, y)
// that.ctx.lineTo(x + w, y + r)
// // 右上角
// that.ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
// // border-right
// that.ctx.lineTo(x + w, y + h - r)
// that.ctx.lineTo(x + w - r, y + h)
// // 右下角
// that.ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5)
// // border-bottom
// that.ctx.lineTo(x + r, y + h)
// that.ctx.lineTo(x, y + h - r)
// // 左下角
// that.ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI)
// // border-left
// that.ctx.lineTo(x, y + r)
// that.ctx.lineTo(x + r, y)
// // that.ctx.fill()
// that.ctx.stroke()
// that.ctx.closePath()
// // 剪切
// that.ctx.clip()
that.ctx.setFillStyle('#fff')
that.ctx.fillRect(0, 0, that.canvasWidth, that.canvasHeight)
that.ctx.drawImage(res[0].path, 0, 0, that.canvasWidth, res[0].height *
ratio);
that.ctx.drawImage(res[1].path, (that.canvasWidth - codeWidth) * 0.5, res[
0].height * ratio + 10, codeWidth, codeWidth);
that.ctx.draw()
that.drawFinish = true
uni.hideLoading()
})
}
});
},
shareLink() {
appModel.shareLink(this.posterData).then(res => {
})
}
},
}
</script>
<style lang="scss">
.shareBox {
display: flex;
justify-content: center;
align-items: center;
justify-content: space-around;
height: 300rpx;
}
.shareCancel {
border-top: 1rpx solid #EEEEEE;
height: 100rpx;
line-height: 100rpx;
text-align: center;
color: #666666;
font-size: 28rpx;
}
.button {
background-color: transparent;
line-height: 0;
padding: 0;
font-size: 0;
/deep/button {
height: 100rpx;
line-height: 1;
width: 300rpx;
margin-top: 60rpx;
color: #4788e9 !important;
background: #FFFFFF !important;
font-weight: bold;
}
}
.shareButton {
background-color: transparent;
line-height: 0;
padding: 0;
font-size: 0;
width: 50%;
margin: 0;
&::after {
border: 0;
}
}
</style>