行业知识
前端uniapp生成海报绘制canvas画布并且保存到相册【实战/带源码/最新】
2026-11-07 13:52:43
来源:互联网转载
目录
- 插件市场
- 效果如下图
- 注意
- 使用my-share.vue
- 插件文件如下图片
- hch-poster
- utils
- index.js
- draw-demo.vue
- hch-poster.vue
- 最后
插件市场
插件市场
效果如下图
注意
主要:使用my-share.vue和绘制canvas的hch-poster.vue这两个使用
使用my-share.vue
{{titleText}}分享图片到 保存海报复制链接微信好友朋友圈保存海报import HchPoster from "../../../../components/hch-poster/hch-poster.vue"// import config from '@/config.js'; // export default {components: {HchPoster},data() {return {posters: [],shareLink: '', // 设置分享链接qrcodeUrl: '', // 设置二维码链接headerImgUrl: '',titleText: '分享人昵称',current: 0, // 设置轮播图标识deliveryFlag: false,posterData: {poster: {//根据屏幕大小自动生成海报背景大小url: '', //图片地址r: 10, //圆角半径w: 300, //海报宽度h: 480, //海报高度p: 20 //海报内边距padding},mainImg: {//海报主商品图url: 'https://huangchunhongzz.gitee.io/imgs/poster/product.png', //图片地址r: 10, //圆角半径w: 250, //宽度h: 200, //高度// w: 250, //宽度// h: 100, //高度// mt: 20, //margin-top// r: 50 //圆角半径},// 分享人昵称文字设置title: {//商品标题text: '', //文本fontSize: 16, //字体大小color: '#FFFFFF', //颜色lineHeight: 25, //行高mt: 10, //margin-top},// 二维码图片// 控制二维码图片移动找import HchPoster from "../../../../components/hch-poster/hch-poster.vue"组件里面的await drawSquarePic这个方法里面codeImg: {//小程序码// url: 'https://huangchunhongzz.gitee.io/imgs/poster/code.png', //图片地址url: '', //图片地址w: 90, //宽度h: 90, //高度mt: 20, //margin-topr: 50, //圆角半径},// 头像图片// 控制头像图片移动找import HchPoster from "../../../../components/hch-poster/hch-poster.vue"组件里面的await drawSquarePic这个方法里面headerImg: {// url: 'https://huangchunhongzz.gitee.io/imgs/poster/code.png', //图片地址url: '', //图片地址w: 50, //宽度h: 50, //高度mt: 10, //margin-topr: 50 //圆角半径},tips: [//提示信息{text: '', //文本fontSize: 16, //字体大小color: '#FFFFFF', //字体颜色align: 'center', //对齐方式lineHeight: 10, //行高mt: 0 //margin-top},{text: '', //文本fontSize: 12, //字体大小color: '#2f1709', //字体颜色align: 'center', //对齐方式lineHeight: 25, //行高mt: 20 //margin-top}]},// 微信好友和朋友圈参数新加参考 产品详情里面// http://localhost:8081/h5/#/pages/product/detail/detailappParams: {title: '',summary: '',path: ''},detail: {},/*分享配置*/shareConfig: {},// logologo: ''}},// 获取初始数据mounted() {},onShow() {// 下面全是引入swiper数据 start!!!this.getCavasSwiperImgData();// 上面全是引入swiper数据 end!!!this.getShareData()},methods: {// 返回资产页面backPrivious() {uni.navigateBack({delta: 1});},// 轮播图标识swiperChange(e) {const {current,source} = e.detail;if (source === 'touch' && current === this.posters.length) {// 如果是用户通过滑动触发的,并且当前滑动到最后一张图this.current = 0; // 将当前索引重置为第一张图的索引} else {this.current = current;}},// 获取邀请海报,二维码和邀请链接async getCavasSwiperImgData() {let self = this;let source = self.getPlatform();uni.showLoading({title: '加载中',});self._get('plus.agent.qrcode/poster', {source: source},res => {uni.hideLoading();if (res.data) {// swiper轮播图数据图片self.posters = res.data.poster;// #ifdef H5// 二维码图片路径self.qrcodeUrl = res.data.qrcode;// #endif// 小程序二维码还没有图片!!!!!!,现在用的是二维码的图片// 除了H5都显示小程序图片// #ifndef H5self.qrcodeUrl = res.data.qrcode;// #endif// 复制链接路径self.shareLink = res.data.url;// 假设头像头像路径self.headerImgUrl = res.data.qrcode;// console.log(res.data.poster,'poster');// console.log(res.data.qrcode,'qrcode');let refereeId = res.data.url.split('?')[1].split('&')[0].split('=')[1];uni.setStorageSync('referee_id', refereeId);// http://localhost:8081/h5/#/pages/product/detail/detail// 之前产品详情是通过,点击打开弹窗触发参数// 调完接口触发微信好友和朋友圈参数//#ifndef H5self.appParams.title = self.detail.product_name;self.appParams.summary = self.detail.product_name;// // 构建页面参数// 这个应该是user_id可能或者图片idlet params = self.getShareUrlParams({product_id: self.product_id});self.appParams.path = '/pages/user/newIndex/my-share/my-share?' + params;self.appParams.image = self.detail.image[0].file_path;self.isAppShare = true;//#endif// self.taskFunc();//分享成功接口方法}});},taskFunc() {let self = this;self._post('plus.task.Task/dayTask', {task_type: 'product'},res => {console.log('分享成功');});},// 点击下载保存相册按钮downloadPoster() {this.posterData.poster.url = this.posters[this.current].file_path;this.posterData.codeImg.url = this.qrcodeUrl// 下面新加this.posterData.headerImg.url = this.headerImgUrlthis.posterData.title.text = this.titleTextthis.$refs.hchPoster.posterShow()this.deliveryFlag = false;},// 取消弹出页面handleClose() {this.deliveryFlag = false},// 点击按钮后复制链接copyLink() {let self = this;uni.setClipboardData({data: self.shareLink,success() {uni.showToast({title: '链接已复制',icon: 'success'});},fail() {uni.showToast({title: '复制失败',icon: 'none'});}});},// 获取分享配置应该是getShareData() {let self = this;self._get('settings/appShare', {},function(res) {self.shareConfig = res.data.appshare;self.logo = res.data.logo;});},// 分享方法share: function(shareType, scene) {let shareOPtions = {provider: "weixin",scene: scene, //WXSceneSession”分享到聊天界面,“WXSenceTimeline”分享到朋友圈type: shareType,success: function(res) {console.log("success:" + JSON.stringify(res));},fail: function(err) {console.log("fail:" + JSON.stringify(err));}}if (this.shareConfig.type != 2) {shareOPtions.summary = this.appParams.summary;shareOPtions.imageUrl = this.logo;shareOPtions.title = this.appParams.title;// 公众号/h5if (this.shareConfig.type == 1) {shareOPtions.href = this.shareConfig.open_site + this.appParams.path;} else if (this.shareConfig.type == 3) {//下载页if (this.shareConfig.bind_type == 1) {shareOPtions.href = this.shareConfig.down_url;} else {shareOPtions.href = config.app_url + "/index.php/api/user.useropen/invite?app_&referee_brush:python;toolbar:false">/* * @Description: 公共方法 * @Version: 1.0.0 * @Autor: hch * @Date: 2021-07-22 00:01:09 *//** * @description: 绘制正方形(可以定义圆角),并且有图片地址的话填充图片 * @param {CanvasContext} ctx canvas上下文 * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {String} url 图片的url地址 */export function drawSquarePic(ctx, x, y, w, h, r, url) {ctx.save()ctx.beginPath()// 绘制左上角圆弧ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)// 绘制border-top// 画一条线 x终点、y终点ctx.lineTo(x + w - r, y)// 绘制右上角圆弧ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)// 绘制border-rightctx.lineTo(x + w, y + h - r)// 绘制右下角圆弧ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5)// 绘制左下角圆弧ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI)// 绘制border-leftctx.lineTo(x, y + r)// 填充颜色(需要可以自行修改)ctx.setFillStyle('#ffffff')ctx.fill()// 剪切,剪切之后的绘画绘制剪切区域内进行,需要save与restore 这个很重要 不然没办法保存ctx.clip()// 绘制图片return new Promise((resolve, reject) => {if (url) {wx.getImageInfo({src: url,success(res) {ctx.drawImage(res.path, x, y, w, h)ctx.restore() //恢复之前被切割的canvas,否则切割之外的就没办法用ctx.draw(true)resolve()},fail(res) {console.log('fail -> res', res)uni.showToast({title: '图片下载异常',duration: 2000,icon: 'none'})}})} else {ctx.draw(true)resolve()}})}/** * @description: 获取设备信息 * @param {type} * @return {type} * @author: hch */export function getSystem() {let system = wx.getSystemInfoSync()let scale = system.windowWidth / 375 //按照苹果留 375*667比例 其他型号手机等比例缩放 显示return {w: system.windowWidth,h: system.windowHeight,scale: scale}}/** * @description: 绘制文本时文本的总体高度 * @param {Object} ctx canvas上下文 * @param {String} text 需要输入的文本 * @param {Number} x X轴起始位置 * @param {Number} y Y轴起始位置 * @param {Number} maxWidth 单行最大宽度 * @param {Number} fontSize 字体大小 * @param {String} color 字体颜色 * @param {Number} lineHeight 行高 * @param {String} textAlign 字体对齐方式 */export function drawTextReturnH(ctx,text,x,y,maxWidth = 375,fontSize = 14,color = '#000',lineHeight = 30,// textAlign = 'left'textAlign = 'center' //文本中心点位置设置) {if (textAlign) {ctx.setTextAlign(textAlign) //设置文本的水平对齐方式 ctx.setTextAlign这个可以兼容百度小程序 ,注意:ctx.textAlign百度小程序有问题switch (textAlign) {case 'center':x = getSystem().w / 2breakcase 'right':x = (getSystem().w - maxWidth) / 2 + maxWidthbreakdefault:// 左对齐x = (getSystem().w - maxWidth) / 2break}}let arrText = text.split('')let line = ''for (let n = 0; n maxWidth && n > 0) {ctx.fillText(line, x, y)line = arrText[n]y += lineHeight} else {line = testLine}}ctx.fillText(line, x, y)ctx.draw(true) //本次绘制是否接着上一次绘制。即 reserve 参数为 false,则在本次调用绘制之前 native 层会先清空画布再继续绘制;若 reserve 参数为 true,则保留当前画布上的内容,本次调用 drawCanvas 绘制的内容覆盖在上面,默认 false。return y}draw-demo.vue
正方形 圆角方形 圆形 图片 左对齐文本 居中对齐文本 右对齐文本 取消 取消 import { drawSquarePic, drawTextReturnH, getSystem } from './utils' export default { data() { return { canvasId: 'canvas', system: {}, canvasShow: false, square1: { //正方形 x: 40, y: 40, r: 0, //圆角半径 w: 80, //宽度 h: 80 //高度 }, square2: { //圆角方形 x: 40, y: 40, r: 10, //圆角半径 w: 80, //宽度 h: 80 //高度 }, square3: { //圆形 x: 40, y: 40, r: 40, //圆角半径 w: 80, //宽度 h: 80 //高度 }, pic1: { x: 40, y: 40, url: 'https://huangchunhongzz.gitee.io/imgs/poster/product.png', r: 0, //圆角半径 w: 250, //宽度 h: 200 //高度 }, text1: { x: 0, y: 40, text: '今日上新水果,牛奶草莓', fontSize: 16, //字体大小 color: '#000', //颜色 lineHeight: 25, //行高 mt: 0 //margin-top }, text2: { x: 0, y: 40, text: '今日上新水果,牛奶草莓', fontSize: 16, //字体大小 color: 'blue', //颜色 lineHeight: 25, //行高 mt: 0, //margin-top align: 'center' //对齐方式 }, text3: { x: 0, y: 40, text: '今日上新水果,牛奶草莓', fontSize: 16, //字体大小 color: 'red', //颜色 lineHeight: 25, //行高 mt: 0, //margin-top align: 'right' //对齐方式 } } }, created() { // 获取设备信息 this.system = getSystem() }, methods: { /** * @description: 展示海报 * @param {type} * @return {type} * @author: hch */ handleDraw(type) { console.log('handleDraw -> type', type) this.canvasShow = true this.draw(type) }, /** * @description: 绘制 * @author: hch */ draw(type) { uni.showLoading({ title: '绘制中...' }) if (this.ctx) { this.ctx.clearRect(0, 0, this.system.w, this.system.h) //清空之前的海报 this.ctx.restore() //恢复之前被切割的canvas,否则切割之外的就没办法用 } else { this.ctx = uni.createCanvasContext(this.canvasId, this) } let drawData = this[type] if (type === 'square1' || type === 'square2' || type === 'square3' || type === 'pic1') { // 绘制图像/图片 drawSquarePic( this.ctx, drawData.x, drawData.y, drawData.w, drawData.h, drawData.r, drawData.url ) } else { // 绘制文本 let textY = drawTextReturnH( this.ctx, drawData.text, drawData.x, drawData.y, this.system.w, drawData.fontSize, drawData.color, drawData.lineHeight, drawData.align ) } uni.hideLoading() }, /** * @description: 取消海报 * @param {type} * @return {type} * @author: hch */ handleCancel() { this.canvasShow = false } } } .content { margin-bottom: 80rpx; overflow: hidden; border-bottom: 1rpx solid $uni-border-color; .btn { float: left; width: 30%; margin: 10rpx; font-size: 30rpx; line-height: 72rpx; color: #fff; text-align: center; background: $uni-btn-color; border-radius: 45rpx; border-radius: 36rpx; } } .canvas-content { position: absolute; top: 0; z-index: 9; .canvas-mask { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 9; width: 100%; height: 100%; background: $uni-btn-color; } .canvas { z-index: 10; } .button-wrapper { position: fixed; bottom: 20rpx; z-index: 16; display: flex; width: 100%; height: 72rpx; justify-content: space-around; } .save-btn { z-index: 16; width: 40%; height: 100%; font-size: 30rpx; line-height: 72rpx; color: #fff; text-align: center; background: $uni-btn-color; border-radius: 45rpx; border-radius: 36rpx; } .cancel-btn { color: $uni-btn-color; background: #fff; } .canvas-close-btn { position: fixed; top: 30rpx; right: 0; z-index: 12; width: 60rpx; height: 60rpx; padding: 20rpx; } }hch-poster.vue
保存海报取消保存海报取消import {drawSquarePic,drawTextReturnH,getSystem} from './utils'export default {data() {return {system: {},canvasShow: false}},props: {posterData: {type: Object,default: () => {return {}}}},computed: {/** * @description: 计算海报背景数据 * @param {*} * @return {*} * @author: hch */poster() {let data = this.posterDatalet system = this.systemlet posterBg = {url: data.poster.url,r: data.poster.r * system.scale,w: data.poster.w * system.scale,h: data.poster.h * system.scale,x: (system.w - data.poster.w * system.scale) / 2,y: (system.h - data.poster.h * system.scale) / 2,p: data.poster.p * system.scale}return posterBg},/** * @description: 计算海报头部主图 * @param {*} * @return {*} * @author: hch */mainImg() {let data = this.posterDatalet system = this.systemlet posterMain = {url: data.mainImg.url,r: data.mainImg.r * system.scale,w: data.mainImg.w * system.scale,h: data.mainImg.h * system.scale,x: (system.w - data.mainImg.w * system.scale) / 2,y: this.poster.y + data.poster.p * system.scale}return posterMain},/** * @description: 计算海报标题 * @param {*} * @return {*} * @author: hch */title() {let data = this.posterDatalet system = this.systemlet posterTitle = data.titleposterTitle.x = this.mainImg.xposterTitle.y = this.mainImg.y + this.mainImg.h + data.title.mt * system.scalereturn posterTitle},/** * @description: 计算小程序码 * @param {*} * @return {*} * @author: hch */codeImg() {let data = this.posterDatalet system = this.systemlet posterCode = {url: data.codeImg.url,r: data.codeImg.r * system.scale,w: data.codeImg.w * system.scale,h: data.codeImg.h * system.scale,x: (system.w - data.codeImg.w * system.scale) / 2,y: data.codeImg.mt * system.scale //y需要加上绘图后文本的y}return posterCode},/** * @description: 计算小程序码 * @param {*} * @return {*} * @author: hch */headerImg() {let data = this.posterDatalet system = this.systemlet posterCode = {url: data.headerImg.url,r: data.headerImg.r * system.scale,w: data.headerImg.w * system.scale,h: data.headerImg.h * system.scale,x: (system.w - data.headerImg.w * system.scale) / 2,y: data.headerImg.mt * system.scale //y需要加上绘图后文本的y}return posterCode}},created() {// 获取设备信息this.system = getSystem()},methods: {/** * @description: 展示海报 * @param {type} * @return {type} * @author: hch */posterShow() {this.canvasShow = truethis.creatPoster()},/** * @description: 生成海报 * @author: hch */async creatPoster() {uni.showLoading({title: '生成海报中...'})const ctx = uni.createCanvasContext('myCanvas', this)this.ctx = ctxctx.clearRect(0, 0, this.system.w, this.system.h) //清空之前的海报ctx.draw() //清空之前的海报// 根据设备屏幕大小和距离屏幕上下左右距离,及圆角绘制背景let poster = this.posterlet mainImg = this.mainImglet codeImg = this.codeImglet headerImg = this.headerImglet title = this.titleawait drawSquarePic(ctx, poster.x, poster.y, poster.w, poster.h, poster.r, poster.url)// 位置移动方法// 先看有没有文字,有文字,按照如下步骤,// 步骤一:先看文本位置,靠左,中,右,然后找到下面方法设置// import {drawTextReturnH} from './utils' 找到drawTextReturnH方法 textAlign = 'center'// 步骤二:根据文本位置设置定位图片// 绘制标题 textY 绘制文本的y位置// 我感觉应该是以这个文本textY位中心点移动,现在文本是居中状态,之前靠左边,文本定位设置在// 这个方法里面drawTextReturnH引入的js方法里面// import {drawTextReturnH} from './utils' 找到drawTextReturnH方法 textAlign = 'center'console.log('creatPoster -> title.x', title.x)// 整体移动 以文本为中心点Y轴上下整体移动,X轴是左,中,右,设置在// import {drawTextReturnH} from './utils' 找到drawTextReturnH方法 textAlign = 'center' right,left里面控制位置let textY = drawTextReturnH(ctx,title.text,title.x,title.y + 180,mainImg.w,title.fontSize,title.color,title.lineHeight)// 步骤二// 头像顶部// 这里控制图片移动位置,X轴,Y轴移动await drawSquarePic(ctx,headerImg.x - 70, //控制X轴移动headerImg.y + textY - 40,//步骤二 Y轴必须加textY这个,应为要以文本为中心点 //控制Y轴移动根据文本textY这个为中心点移动headerImg.w,headerImg.h,0,headerImg.url)// 头像底部// 步骤二// 绘制小程序码// 现在更换接口是二维码图片// 这里控制图片移动位置,X轴,Y轴移动await drawSquarePic(ctx,codeImg.x + 90, //控制X轴移动codeImg.y + textY - 70, //步骤二 Y轴必须加textY这个,应为要以文本为中心点 //控制Y轴移动根据文本textY这个为中心点移动// codeImg.x, //控制X轴移动// codeImg.y, //控制Y轴移动codeImg.w,codeImg.h,0,codeImg.url)// 小程序的名称// 长按/扫描识别查看商品let y = 0this.posterData.tips.forEach((element, i) => {if (i == 0) {y = codeImg.y + textY + element.mt + codeImg.h} else {y += element.mt}y = drawTextReturnH(ctx,element.text,title.x,y,mainImg.w,element.fontSize,element.color,element.lineHeight,element.align)})uni.hideLoading()},/** * @description: 保存到系统相册 * @param {type} * @return {type} * @author: hch */handleSaveCanvasImage() {uni.showLoading({title: '保存中...'})let _this = this// 把画布转化成临时文件// #ifndef MP-ALIPAY// 支付宝小程序外,其他都是用这个方法 canvasToTempFilePathuni.canvasToTempFilePath({x: this.poster.x,y: this.poster.y,width: this.poster.w, // 画布的宽height: this.poster.h, // 画布的高destWidth: this.poster.w * 5,destHeight: this.poster.h * 5,canvasId: 'myCanvas',success(res) {//保存图片至相册// #ifndef H5// 除了h5以外的其他端uni.saveImageToPhotosAlbum({filePath: res.tempFilePath,success(res) {uni.hideLoading()uni.showToast({title: '图片保存成功,可以去分享啦~',duration: 2000,icon: 'none'})_this.handleCanvasCancel()},fail() {uni.showToast({title: '保存失败,稍后再试',duration: 2000,icon: 'none'})uni.hideLoading()}})// #endif// #ifdef H5// h5的时候uni.showToast({title: '请长按保存',duration: 3000,icon: 'none'})_this.handleCanvasCancel()_this.$emit('previewImage', res.tempFilePath)// #endif},fail(res) {console.log('fail -> res', res)uni.showToast({title: '保存失败,稍后再试',duration: 2000,icon: 'none'})uni.hideLoading()}},this)// #endif// #ifdef MP-ALIPAY// 支付宝小程序条件下 toTempFilePaththis.ctx.toTempFilePath({x: this.poster.x,y: this.poster.y,width: this.poster.w, // 画布的宽height: this.poster.h, // 画布的高destWidth: this.poster.w * 5,destHeight: this.poster.h * 5,success(res) {//保存图片至相册my.saveImage({url: res.apFilePath,showActionSheet: true,success(res) {uni.hideLoading()uni.showToast({title: '图片保存成功,可以去分享啦~',duration: 2000,icon: 'none'})_this.handleCanvasCancel()},fail() {uni.showToast({title: '保存失败,稍后再试',duration: 2000,icon: 'none'})uni.hideLoading()}})},fail(res) {console.log('fail -> res', res)uni.showToast({title: '保存失败,稍后再试',duration: 2000,icon: 'none'})uni.hideLoading()}},this)// #endif},/** * @description: 取消海报 * @param {type} * @return {type} * @author: hch */handleCanvasCancel() {this.canvasShow = falsethis.$emit('cancel', true)}}}$uni-btn-color: #007aff;.content {height: 100%;text-align: center;}.canvas-content {position: absolute;top: 0;.canvas-mask {position: fixed;top: 0;right: 0;bottom: 0;left: 0;z-index: 9;width: 100%;height: 100%;background: rgba(0, 0, 0, 0.5);}.canvas {z-index: 10;}.button-wrapper {position: fixed;bottom: 20rpx;z-index: 16;display: flex;width: 100%;height: 72rpx;justify-content: space-around;}.save-btn {z-index: 16;width: 40%;height: 100%;font-size: 30rpx;line-height: 72rpx;color: #fff;text-align: center;background: $uni-btn-color;border-radius: 45rpx;border-radius: 36rpx;}.cancel-btn {color: $uni-btn-color;background: #fff;}.canvas-close-btn {position: fixed;top: 30rpx;right: 0;z-index: 12;width: 60rpx;height: 60rpx;padding: 20rpx;}}最后
感觉文章好的话记得点个心心和关注和收藏,有错的地方麻烦指正一下,如果需要转载,请标明出处,多谢!!!
上一篇:服务器怎么叫车?