uniapp使用wxml-to-canvas开发小程序保存canvas图片

发布时间:2024年01月10日

微信小程序官方解决方案:wxml-to-canvas

使用wxml-to-canvas要知道一些前提条件
1、只能画view,text,image
2、每个元素必须要设置宽高
3、默认是flex布局,可以通过flexDirection: "column"来改变排列方式
4、文字 必须放在text中,放在view中无法显示,且text不支持字体加粗
5、如果你要设置背景颜色,请使用backgroundColor,而非background,border同理~
6、多个absolute元素时,因为没有z-index,template元素自上而下渲染,对应z-index依次增高
7、导出图片过大,可以通过canvasToTempFilePath({fileType, quality})配置里面的quality字段来减小
8、image只支持临时地址和网络地址,不支持base64和本地图片,可以通过writeFile 把base64转成临时地址
如下:
const fs = wx.getFileSystemManager();
let qrcodeBase64 = QRresult.data;
let qrcodeLink = `${wx.env.USER_DATA_PATH}/qrcodeLink.gif`;
fs.writeFile({
    filePath: qrcodeLink,
    data: qrcodeBase64,
    encoding: 'base64',
    success: res => {
        console.log(res)
        shopJson.qrcode = qrcodeLink;
        this.renderToCanvas();
    },
    fail(res) {
        console.error(res)
    }
})


9、Canvas 初始化问题:为了正确绘制图像,需要确保在调用 canvas.draw() 之前完成所有绘制操作。
10、不支持实时更新:一旦 Canvas 绘制完成,它将成为静态图像,无法实时更新。如果需要实时更新数据可视化图表,可以考虑
使用其他库或组件。
11、长按保存图片问题:wxml-to-canvas 组件默认无法通过长按保存图片功能保存 Canvas 图像。你可能需要自行实现该功能,
并适配不同平台的实现方式。
12、对低版本小程序的兼容性问题:某些较旧的微信小程序版本可能无法完全支持 wxml-to-canvas 组件。在开发之前,请确保目
标用户群体的微信小程序版本。
13、性能问题:复杂的数据可视化图表可能会影响性能,尤其是在绘制大量数据时。尽量优化绘制逻辑,以避免出现卡顿或延迟。
14、与其他组件的兼容性问题:wxml-to-canvas 组件可能与其他小程序组件存在一些兼容性问题,导致样式错乱或布局问题。建
议在使用时进行充分的测试和调试。
15、跨平台问题:wxml-to-canvas 组件目前主要针对微信小程序,可能无法直接适用于其他小程序平台或移动端框架。如果需要
在其他平台上实现类似的功能,可能需要另行寻找适合的解决方案。
16、Canvas 绘图能力限制:由于 Canvas 的绘图能力有限,某些高级的数据可视化需求可能无法直接通过 wxml-to-canvas 组
件实现。在确定方案之前,建议先了解 Canvas 绘图的限制和特性。
17、样式定制问题:某些样式属性或效果可能难以通过 wxml-to-canvas 组件实现,例如阴影、渐变色等。需要根据具体需求考虑
是否能够满足所需的样式效果。
18、开发者工具与真机表现差异:在进行调试和预览时,开发者工具上的表现可能与真机上存在一些差异。建议进行真机测试,以
确保数据可视化图表在不同设备上正常显示。

等等...其他未知问题...

一、安装

npm install --save wxml-to-canvas

二、在程序根目录下新建?wxcomponents?文件夹,将node_modules下的?widget-ui?和?wxml-to-canvas?两个文件夹复制进去。

注意:这里有的安装后生成的node_modules还不一样,可直接复制dist里面的即可(如下图)

?三、将/wxcomponents/wxml-to-canvas/index.js中的

module.exports = require("widget-ui");
//改为
module.exports = require("../widget-ui/index.js")

?四、配置pages.json (这样uni-app才会打包wxcomponents)

  • 1. 在需要用到的页面配置
"pages": [
    {
        "path": "pages/xxx",
        "style": {
            "usingComponents": {
              "wxml-to-canvas": "/wxcomponents/wxml-to-canvas/index"
            }
        }
    },
...
]
  • 2. 或者在globalStyle里面全局配置
"globalStyle": {
		"navigationBarTextStyle": "black",
		"navigationBarTitleText": "uni-app",
		"navigationBarBackgroundColor": "#F8F8F8",
		"backgroundColor": "#F8F8F8",
		"usingComponents": {
			"wxml-to-canvas": "/wxcomponents/wxml-to-canvas/index"
		}
},

五、重点来了,如何使用?

  • ?页面配置

<!-- 测试页面,可复制使用 -->
<template>
  <view class="wrap">
    <!-- 保存canvas测试 -->
    <view class="share-page">
      <view class="share-page-box" id="box" v-if="show"
        :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }">
        <wxml-to-canvas class="widget" :width="canvasWidth" :height="canvasHeight"></wxml-to-canvas>
      </view>
      <view class="share-page-box msg-box" v-else :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }">
        {{ msg }}
      </view>
      <view class="share-page-btn" @tap="extraImage">
        <button class="btn-big" :style="getBtnStyle">保存图片</button>
      </view>
    </view>
  </view>
</template>

<script>
import { wxml, style } from './DomData';
export default {
  data() {
    return {
      show: false, // 是否显示canvas
      canvasWidth: 320, // 默认canvas宽高
      canvasHeight: 480,
      screenWidth: null, // 设备宽度
      screenHeight: null, // 设备高度
      // name: '',
      // pic: '',
      // chapter1: '',
      // chapter2: '',
      widget: null,
      msg: '加载中,请稍等...', // 提示语
      listData:[
        {
          type:'1',
          title:'标题一',
          auther: '张三  2024-01-10',
          imgsrc:'https://pic1.zhimg.com/80/v2-58fe538a59f870407b1435bfd45893ed_720w.jpeg',
          text:'本报讯(记者 杜晨薇)上海正在建设东方枢纽项目,浦东国际机场与沪通铁路将实现连通,枢纽周边临空产业、重点项目布局正在规划设计中。为落实上海市政府与中国东方航空集团有限公司(以下简称“中国东航”)合作协议要求,充分利用东方枢纽建设发展机遇期进一步深化合作发展,昨天下午,浦东新区政府与中国东航签署深化合作战略协议。'
        },
        {
          type: '2',
          title: '标题二',
          auther: '张三  2024-01-10',
          imgsrc: 'https://pic1.zhimg.com/80/v2-58fe538a59f870407b1435bfd45893ed_720w.jpeg',
          text: '本报讯(记者 杜晨薇)上海正在建设东方枢纽项目,浦东国际机场与沪通铁路将实现连通,枢纽周边临空产业、重点项目布局正在规划设计中。为落实上海市政府与中国东方航空集团有限公司(以下简称“中国东航”)合作协议要求,充分利用东方枢纽建设发展机遇期进一步深化合作发展,昨天下午,浦东新区政府与中国东航签署深化合作战略协议。'
        },
        {
          type: '3',
          title: '标题三',
          auther: '张三  2024-01-10',
          imgsrc: 'https://pic1.zhimg.com/80/v2-58fe538a59f870407b1435bfd45893ed_720w.jpeg',
          text: '本报讯(记者 杜晨薇)上海正在建设东方枢纽项目,浦东国际机场与沪通铁路将实现连通,枢纽周边临空产业、重点项目布局正在规划设计中。为落实上海市政府与中国东方航空集团有限公司(以下简称“中国东航”)合作协议要求,充分利用东方枢纽建设发展机遇期进一步深化合作发展,昨天下午,浦东新区政府与中国东航签署深化合作战略协议。'
        },
        {
          type: '4',
          title: '标题四',
          auther: '张三  2024-01-10',
          imgsrc: 'https://pic1.zhimg.com/80/v2-58fe538a59f870407b1435bfd45893ed_720w.jpeg',
          text: '本报讯(记者 杜晨薇)上海正在建设东方枢纽项目,浦东国际机场与沪通铁路将实现连通,枢纽周边临空产业、重点项目布局正在规划设计中。为落实上海市政府与中国东方航空集团有限公司(以下简称“中国东航”)合作协议要求,充分利用东方枢纽建设发展机遇期进一步深化合作发展,昨天下午,浦东新区政府与中国东航签署深化合作战略协议。'
        },
        {
          type: '5',
          title: '标题五',
          auther: '张三  2024-01-10',
          imgsrc: 'https://pic1.zhimg.com/80/v2-58fe538a59f870407b1435bfd45893ed_720w.jpeg',
          text: '本报讯(记者 杜晨薇)上海正在建设东方枢纽项目,浦东国际机场与沪通铁路将实现连通,枢纽周边临空产业、重点项目布局正在规划设计中。为落实上海市政府与中国东方航空集团有限公司(以下简称“中国东航”)合作协议要求,充分利用东方枢纽建设发展机遇期进一步深化合作发展,昨天下午,浦东新区政府与中国东航签署深化合作战略协议。'
        },
      ]
    }
  },

  methods: {
    // wxml 转 canvas
    renderToCanvas() {
      console.log('canvasStyle.widget', this.widget)
      const _wxml = wxml(this.listData);
      console.log('this.widget', _wxml)
      const _style = style(this.screenWidth, this.canvasWidth, this.canvasHeight) //this.canvasHeight
      const p1 = this.widget.renderToCanvas({ wxml: _wxml, style: _style })
      console.log('renderToCanvas', p1)
      p1.then((res) => {
        console.log('海报生成成功', res);
        wx.hideLoading()
      }).catch((err) => {
        console.log('生成失败', err)
      })
    },
    // 保存到朋友圈
    extraImage() {
      if (!this.show) {
        wx.showToast({ title: '海报生成失败,无法分享到朋友圈', icon: 'none' })
        return
      }
      wx.showLoading({ title: '海报生成中...' })
      const p2 = this.widget.canvasToTempFilePath({ fileType:'jpg', quality :0.5})
      let that = this;
      p2.then(result => {
        let path = result.tempFilePath
        wx.getSetting({
          success: res => {
            wx.hideLoading()
            // 非初始化且未授权的情况,需要再次弹窗提示授权
            if (res.authSetting['scope.writePhotosAlbum'] != undefined && res.authSetting['scope.writePhotosAlbum'] != true) {
              wx.showModal({
                title: '是否授权相册权限',
                content: '需要获取相册权限,请确认授权,否则无法使用相关功能',
                success: res => {
                  if (res.confirm) {
                    wx.openSetting({
                      success: dataAu => {
                        if (dataAu.authSetting["scope.writePhotosAlbum"] == true) {
                          wx.showToast({
                            title: '授权成功',
                            icon: 'none',
                            duration: 1000
                          });
                          that.saveIMg(path);
                        } else {
                          wx.showToast({
                            title: '授权失败',
                            icon: 'success',
                            duration: 1000
                          });
                        }
                      }
                    });
                  }
                }
              });
            } else {
              // 初始化且未授权,系统默认会弹窗提示授权
              // 非初始化且已授权,也会进入这里
              that.saveIMg(path);
            }
          }
        });
      })
    },
    // 保存到相册
    async saveIMg(tempFilePath) {
      wx.saveImageToPhotosAlbum({
        filePath: tempFilePath,
        success: async (res) => {
          wx.showModal({
            content: '图片已保存,分享给好友吧!',
            showCancel: false,
            confirmText: '好的',
            confirmColor: '#333',
            success: function (res) {
              wx.navigateBack({
                //返回
                delta: 1
              });
            },
            fail: function (res) {
              console.log('res', res);
            }
          });
        },

        fail: function (res) {
          wx.showToast({
            title: '您取消了授权',
            icon: 'none',
            duration: 2000
          })
        }
      });
    },
  },
  onLoad(options) {
    console.log('options', options);
    // 获取设备信息
    wx.getSystemInfo({
      success: (res) => {
        console.log('屏幕',res)
        this.screenWidth = res.screenWidth;
        this.canvasWidth = this.screenWidth;
        this.canvasHeight = this.screenWidth * 8.5;
        console.log('海报高度:', this.canvasHeight)

        this.show = true
        // 数字容器宽度 动态设置 
        setTimeout(() => {
          wx.showLoading({ title: '海报加载中...' })
          this.widget = this.selectComponent('.widget')
          this.renderToCanvas()
        }, 1000)
      }
    });
  },
}


</script>

<style lang="scss">
.share-page {
  background: #cc0202;
  position: relative;
  overflow: hidden;
  // padding: 10rpx;
//   min-height: 100vh;
  .msg-box {
    display: flex;
    align-items: center;
    text-align: center;
    justify-content: center;
  }
  .share-page-box {
    margin: 0 auto;
    position: relative;
    overflow: hidden;
	  box-shadow: 0rpx 6rpx 20rpx 6rpx rgba(0, 0, 0, 0.2);
  }
  .share-page-btn {
    margin: 0 10rpx 0 10rpx;
    img {
      width: 100%;
      height: 100%;
    }
  }
}
</style>

  • DomData.js

/**
 *
 * @param {*} listData  canvas数据
 */

export const wxml = (listData) => `
<view class="container">
  ${listData.map(item=>{
    return `
    <view class="contentWrap">
      <view >
        <image src="`+ item.imgsrc + `"  class="pic1"/>
        <text class="name">`+ item.title + `</text>
        <text class="subtitle">`+ item.auther + `</text>
      </view>
      <view class="bottomcss">
        <image src="`+ item.imgsrc + `"  class="imgbc"/>
        <view class="tapContent1">
          <text class="tapname">`+ item.text+`</text>
        </view>
      </view>
    </view>`
  }).join('')}
</view>
`
/**
 *
 *
 * @param {*} screenWidth 屏幕宽度
 * @param {*} canvasWidth  画布宽度
 * @param {*} canvasHeight  画布高度
 * @param {*} numberWidth  数字宽度,动态设置
 * @return {*} 
 */
 export const style = (screenWidth, canvasWidth, canvasHeight) => {
  return {
    "container": {
      width: canvasWidth,
      minHeight: canvasHeight,
      position:'relative',
      backgroundColor: '#ffffff',
      justifyContent: 'center',
      alignItems:'center',
      overflow: 'hidden'
    },
    "bottomcss":{
      marginTop: 0,
    },

    "contentWrap":{
      position: 'relative',
      width: canvasWidth * 0.99,
      marginBottom: 20,
      marginTop: 5,
      marginLeft: 1,
      borderRadius: 20,
      overflow:'hidden',
      backgroundColor: '#333333',
    },
    "imgbc":{
      justifyContent: 'center',
      alignItems: 'center',
      width: canvasWidth * 0.97,
      height: canvasWidth,
      marginBottom: 4,
      marginLeft: 4,
      marginTop: 10,
      borderRadius: 20,
      overflow: 'hidden',
    },
    "tapContent1":{
      position:'absolute',
      top: 0,
      left: 0,
    },
    "tapname":{
      fontSize: 18,
      color: '#fff',
      marginLeft: 15,
      marginTop: 50,
      overflow: 'hidden',
      width: canvasWidth * 0.92,
      height: 400,
      textAlign: 'left',
    },
    "name":{
      fontSize: 20,
      color: '#fff',
      marginLeft: canvasWidth * 0.08,
      width: canvasWidth * 0.84,
      height: 30,
			textAlign: 'center',
    },
    "subtitle":{
      fontSize: 14,
      color: '#9E9C9C',
      marginLeft: canvasWidth * 0.08,
      width: canvasWidth * 0.84,
      height: 20,
      textAlign: 'center',
    },
    "content": {
			fontSize: 14,
			color: '#333',
      width: canvasWidth * 0.84,
      height: screenWidth * 0.15,
			marginLeft: canvasWidth * 0.08,
    },
    "pic1": {
      width: canvasWidth * 0.3,
      height: screenWidth * 0.3,
      marginTop: canvasWidth * 0.1,
      marginLeft: canvasWidth * 0.35,
			marginBottom: canvasWidth * 0.05,
			borderRadius: screenWidth * 0.14,
			overflow: 'hidden',
    },
    "pic2": {
      width: canvasWidth ,
      height: canvasWidth ,
      marginTop:10,
    },
    "bottom":{
      width: canvasWidth,
      height: screenWidth * 0.2,
      flexDirection: 'row',
      justifyContent: 'self-start',
      alignItems: 'center',
			backgroundColor: '#fafafa',
      position: 'absolute',
      bottom: 0,
      left: 0,
    },
		"qr": {
		  width: canvasWidth * 0.14,
		  height: screenWidth * 0.14,
		  marginLeft: canvasWidth * 0.04,
			marginRight: canvasWidth * 0.04,
		},
    "msg": {
      fontSize: 14,
      color: '#a1a1a1',
      width: canvasWidth * 0.74,
			height: 14,
			textAlign: 'left'
    },
  }
}


?


六、生成的效果图

?


...... 全 剧 终 ......

文章来源:https://blog.csdn.net/QQ_Empire/article/details/135501333
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。