uniapp微信小程序长截屏

接到一个需求是用户要截长屏并转发到工作群,但是微信小程序获取不到dom,于是用canvas绘制页面后导出。

效果


VeryCapture_20240819150308.gif

实现过程:

  1. 根据页面内容设置canvas画布的框高
  2. js获取html标签中的信息(标签类型,字号,字重,颜色,背景等),整理并传递给封装的canvas组件
  3. canvas组件获取信息将内容绘制到画布上
  4. 将画布导出为图片

1.根据页面内容设置canvas画布的框高

// 获取页面内容宽高 
  getSystemInfo(height) {
      let that = this
      uni.getSystemInfo({
        success: function (info) {
          that.canvasWidth = info.windowWidth * info.pixelRatio
          that.canvasHeight = height * info.pixelRatio
        }
      });
    }

tip: 在获取页面宽高时需要置顶整个页面,否则最后导出的图片可能被截断

    uni.pageScrollTo({
        scrollTop: 0,
        duration: 0
      })

2. js获取html标签中的信息(标签类型,字号,字重,颜色,背景等),整理并传递给封装的canvas组件

需求里要导出的页面里只简单的标签:view,text, image,可以通过data-set传递标签信息
data-set用法示例

// 引入封装的canvas组件
    <export-img-canvas ref="imgCanvas" 
        :canvas-width="canvasWidth" 
        :canvas-height="canvasHeight"
        @sharePicture="sharePicture"
        />
// 页面搭建
<view class="testClass" data-type="view">
   <text class="testClass" data-type="text" data-word="详情" data-size="40" data-color="#333" data-weight="bold">详情</text>
   <image class="testClass" data-type="image"  v-for="item in imgList" :key="item" src="item" style="width: 1080;height:960rpx;" :data-src="item" />
 </view>

在js中可以通过uni.createSelectorQuery().selectAll('.testClass').boundingClientRect().exec()获取class为testClass的信息

    uni.createSelectorQuery().selectAll('.testClass').boundingClientRect().exec(async (res)=>{
        console.log('获取到信息:',res[0])
        let resArray = res[0]
        await that.getSystemInfo(resArray[0].height) // 获取页面内容宽高
        for(let i=0;i<resArray.length;i++){
          const { width, height, left, top } = resArray[i]
          //1.判断查询到的组件类别,如果是view类,就按照view对应的格式将创建的对象添加到数组中
          if(resArray[i].dataset.type == 'view'){
            console.log('view')
            let radius = resArray[i].dataset.radius
            let bgColor = resArray[i].dataset.bgcolor
            let shadow = resArray[i].dataset.shadow
            let drawRect = resArray[i].dataset.drawRect
            arrayInfo.push({
              width, height,
              posX: left,
              posY: top,
              type: 'view',
              radius: radius,
              shadow: shadow ? true : false,
              bgColor: bgColor,
              drawRect: drawRect ? true : false,
            })
          }
          //2.判断查询到的组件类别,如果是image类,就按照image对应的格式将创建的对象添加到数组中
          else if(resArray[i].dataset.type == 'image'){
            console.log('image')
            let src= resArray[i].dataset.src
            arrayInfo.push({
              width, height,
              posX: left,
              posY: top,
              type:'image',
              src: src,
            })
          }
          //3.判断查询到的组件类别,如果是text类,就按照text对应的格式将创建的对象添加到数组中
          else if(resArray[i].dataset.type == 'text'){
            console.log('text')
            const { color, word, size, weight } = resArray[i].dataset
            arrayInfo.push({
              color, word, size, weight,
              posX: left,
              posY: top,
              type: 'text',
            })
          }
        }
        //查询完毕后,也就创建好了arrayInfo数组,下面将arrayInfo数组作为参数,传给canvas组件的drawCanvas()方法
        that.$refs.imgCanvas.drawCanvas(arrayInfo)
      })

3. export-img-canvas组件获取信息将内容绘制到画布上

组件分装如下,已写好备注

<template>
    <view>
    </view>
</template>
    let promiseIndex = 0
    let promiseArray = []
    let imageArray = []
    let imageIndex = 0
    export default {
        data() {
            return {

            };
        },
        props: {
            canvasWidth: {
                type: Number,
                default: 0
            },
            canvasHeight: {
                type: Number,
                default: 0
            }
        },
        methods: {
            //rpx转换为px的工具函数
            rpxtopx(rpx) {
                let deviceWidth = wx.getSystemInfoSync().windowWidth; //获取设备屏幕宽度
                let px = (deviceWidth / 750) * Number(rpx)
                return Math.floor(px);
            },

            // 绘制阴影 
            drawShadow(ctx, dpr, bool) {
                if(bool) {
                    ctx.shadowOffsetX = 0; // 阴影在x轴的偏移量
                    ctx.shadowOffsetY = 0; // 阴影在y轴的偏移量
                    ctx.shadowBlur = 14 * dpr; // 阴影的模糊程度
                    ctx.shadowColor = 'rgba(186,186,186,0.5)'; // 阴影的颜色和透明度
                } else {
                    ctx.shadowColor = 'transparent';
                    ctx.shadowBlur = 0;
                    ctx.shadowOffsetX = 0;
                    ctx.shadowOffsetY = 0;
                }
            },

            // 绘制矩形 
            drawRect(ctx, radius, x, y, width, height) {
                if(radius) {
                    console.log(x, y, width, height, radius, 'radius=====');
                    // 圆角 
                    if (ctx.roundRect) {
                        console.log('roundRect======');
                        ctx.roundRect(x, y, width, height, [radius]);
                    }else {
                        ctx.beginPath();
                        ctx.moveTo(x+radius, y);
                        ctx.arcTo(x+width, y, x+width, y+height, radius);
                        ctx.arcTo(x+width, y+height, x, y+height, radius);
                        ctx.arcTo(x, y+height, x, y, radius);
                        ctx.arcTo(x, y, x+width, y, radius);
                        ctx.closePath();
                    }
                    ctx.fill();
                } else {
                    // 非圆角
                    ctx.fillRect(x, y, width, height)
                }

            },

            // 绘制背景
            drawBgColor(ctx, dpr, bgColor) {
                if(!bgColor) {
                    ctx.fillStyle = 'rgba(255,255,255,0)' // 默认透明
                } else if (bgColor === 'gradient') {
                    let gradient = ctx.createLinearGradient(0, 0, 0, 800);
                    gradient.addColorStop(0, "pink");
                    gradient.addColorStop(1, "#FFFFFF");
                    ctx.fillStyle = gradient;
                    ctx.fillRect(0, 0, 1600, 800);
                } else {
                    ctx.fillStyle = bgColor
                }
            },

            // 绘制字体样式 
            drawFont(ctx, color, size, weight, word, posX, posY) {
                ctx.fillStyle = color ? color : '#333'
                ctx.font = weight ? 'bold ' + size + 'px PingFang-SC' : size + 'px PingFang-SC'
                ctx.textBaseline = 'middle'
                ctx.fillText(word, posX, posY)
            },

            // 绘制canvas 
            drawCanvas(arrayInfo) {
                promiseIndex = 0
                promiseArray = []
                imageArray = []
                imageIndex = 0
                console.log('数据信息:', arrayInfo)
                // 0.创建canvas及ctx,并获取屏幕的dpr
                const canvas = wx.createOffscreenCanvas({
                    type: '2d',
                    width: this.canvasWidth,
                    height: this.canvasHeight
                })
                const ctx = canvas.getContext('2d')

                const dpr = uni.getWindowInfo().pixelRatio

                // 1.根据数组数量创建promise数组
                for (let i = 0; i < arrayInfo.length; i++) {
                    promiseArray.push(promiseIndex)
                    promiseIndex++
                }


                // 2.根据数组给promise数组赋值
                let that = this
                for (let i = 0; i < arrayInfo.length; i++) {
                    promiseArray[i] = new Promise(function(resolve, reject) {
                        if (arrayInfo[i].type == 'text') {
                            resolve()
                        } else if (arrayInfo[i].type == 'view') {
                            resolve()
                        } else if (arrayInfo[i].type == 'image') {
                            let img = canvas.createImage()
                            img.src = arrayInfo[i].src
                            img.onload = function() {
                                imageArray.push(img)
                                resolve()
                            }
                            img.src = arrayInfo[i].src

                        }
                    })
                }

                // 3.异步全部加载完成后,绘制图片
                let p = Promise.all(promiseArray)
                p.then(function() {
                    //根据数组顺序绘制图片
                    for (let i = 0; i < arrayInfo.length; i++) {
                        if (arrayInfo[i].type == 'text') {
                            console.log('绘制文字')
                            let { color, size, weight, word, posX, posY } = arrayInfo[i]
                            size = that.rpxtopx(arrayInfo[i].size) * dpr
                            posX = posX * dpr
                            posY = (posY + that.rpxtopx(20)) * dpr
                            that.drawFont(ctx, color, size, weight, word, posX, posY)
                        } else if (arrayInfo[i].type == 'view') {
                            console.log('绘制view')
                            that.drawShadow(ctx, dpr, arrayInfo[i].shadow)
                            that.drawBgColor(ctx, dpr, arrayInfo[i].bgColor)
                            that.drawRect(ctx, arrayInfo[i].radius, arrayInfo[i].posX * dpr, arrayInfo[i].posY * dpr, arrayInfo[i].width *
                                dpr, arrayInfo[i].height * dpr)
                            
                        } else if (arrayInfo[i].type == 'image') {
                            console.log('33', imageArray)
                            console.log('绘制图片')
                            ctx.drawImage(imageArray[imageIndex], arrayInfo[i].posX * dpr, arrayInfo[i].posY * dpr,
                                arrayInfo[i].width * dpr, arrayInfo[i].height * dpr)
                            imageIndex++
                        }
                    }

                    const path = `${wx.env.USER_DATA_PATH}/`+ new Date().getTime()+`hello.png`

                    // 初始化数据
                    promiseIndex = 0
                    promiseArray = []
                    imageArray = []
                    imageIndex = 0

                    // 将生成的图片传递到父组件
                    that.$emit('sharePicture', { canvas, path })
                    
                })
            }
        }
    }

4. 将画布导出为图片

// 分享图片 
    sharePicture({ canvas, path }) {
      const fileData = canvas.toDataURL()
      const fs = uni.getFileSystemManager()
      let that = this
      fs.writeFile({
        filePath: path,
        data: fileData.replace(/^data:image\/\w+;base64,/, ""),
        encoding: 'base64',
        success(res) {
          uni.hideLoading()
          wx.showShareImageMenu({
            withShareTicket: true,
            path: path,
            success: async res => {
              console.log(res)
              uni.navigateBack()
            }
          })
        },
        complete(res) {
          console.log('调用结束');
          // uni.navigateBack()
        }
      })
    },

————————

完整代码补充如下:

1. html部分

<template>
  <view class="transpond">
    <view class="testClassu-p-b-20" data-type="view" data-bgcolor="#fff">
      <export-img-canvas ref="imgCanvas" 
        :canvas-width="canvasWidth" 
        :canvas-height="canvasHeight"
        @sharePicture="sharePicture"
        />
      <view class="position-box testClass" data-type="view" data-bgcolor="gradient"></view>
      <view class="title-name testClass" data-type="view">
        <text class="testClass" data-word="测试详情" data-size="40" data-color="#333" data-type="text" data-weight="bold">测试详情</text>
      </view>
      <!-- 测试信息  -->
      <view class='item-box testClass' data-type="view" data-bgcolor="#fff" :data-shadow="true">
        <view class="title-info testClass" data-type="view" data-bgcolor="#fff">
          <text class="testClass" data-word="测试信息" data-weight="bold" data-size="32" data-color="#333" data-type="text">测试信息</text>
        </view>
        <view class="user-info testClass" data-type="view" data-bgcolor="#fff">
          <view class="testClass" data-type="view" data-bgcolor="#fff">
  
            <view class="testClassuser-info-item" data-type="view" v-for="(item, index) in testInfo" :key="index">
              <view class="testClass" data-type="view">
                <text class="testClass"  data-size="28" data-color="#333" data-type="text" data-weight="bold"
                  :data-word="item">{{ item }}</text>
              </view>
              <view class="testClass" data-type="view">
                <text class="testClass"  data-size="28" data-color="#333" data-type="text"
                  :data-word="info[index]">{{ info[index] }}</text>
              </view>
            </view>
  
          </view>
          <view class="testClassicon-box" data-type="view" data-bgcolor="pink">
            <view>
              <view class="testClassnumber-info" data-type="view">
                <text class="testClass" data-size="28" data-color="#FFFFFF" data-type="text"
                  data-word="测试" >测试</text>
              </view>
            </view>
          </view>
        </view>
      </view>
  
      <!-- 表格  -->
      <view class="table-box"  data-roundRect="20">
        <view class="table-title testClass" data-type="view" data-bgcolor="pink">
          <view class="testClasscenter-box" data-type="view" v-for="(item, index) in tableInfo" :key="index" :style="{ width: tableWidthInfo[index]+'rpx' }">
            <text class="testClass" data-color="#FFFFFF" data-type="text" data-size="28" :data-word="item">
              {{ item }}
            </text>
          </view>
        </view>
        <template v-for="(item, index) in info.detailList">
          <view class="table-content testClass" data-type="view" data-bgcolor="#fffcfb">
            <view class="testClasscenter-box" data-type="view" v-for="(child, childIndex) in tableInfo" :key="childIndex" :style="{ width: tableWidthInfo[childIndex]+'rpx' }">
              <text class="testClass" data-size="24" data-color="#333" data-type="text" :data-word="item[childIndex]" >
                {{ item[childIndex] }}
              </text>
            </view>
          </view>
        </template>
      </view>

      <!-- 底部  -->
      <view class="title-name testClass" data-type="view">
        <text class="testClass" data-word="----底部----" data-size="40" data-color="#333" data-type="text" data-weight="bold">----底部----</text>
      </view>

    </view>
    <button @tap="toCanvas">生成图片</button>
  </view> 
</template>

2.js部分

import exportImgCanvas from './exportImgCanvas'
let arrayInfo = []
export default {
  components: { exportImgCanvas },
  data() { 
    return {
      canvasWidth: 1080,
      canvasHeight: 0,
      info: {},
      testInfo: {
        testid: '测试编号:',
        name: '测试名:',
        bdTest: '测试库:',
        finishedtime: '测试时间:',
        testid2: '测试编号2:',
        name2: '测试名2:',
        bdTest2: '测试库2:',
        finishedtime2: '测试时间2:',
      },
      tableWidthInfo: {
        index: 80,
        gname: 200,
        unitprice: 160,
        weight: 80,
        amount: 100
      },
      tableInfo: {
        gname: '物品',
        unitprice: '价格',
        weight: '数量',
        amount: '总价'
      },
    }
  },
  mounted() {
    this.info = {
      testid: '88888',
      name: '99999',
      bdTest: 'DB-test',
      finishedtime: '2024-08-19',
      testid2: '88888',
      name2: '99999',
      bdTest2: 'DB-test',
      finishedtime2: '2024-08-19',
      detailList: [
        { index: 1, gname: '茶叶', unitprice: '30', weight: '10', amount: '300'},
        { index: 2, gname: '肥皂', unitprice: '30', weight: '10', amount: '300'},
        { index: 3, gname: '杯子', unitprice: '30', weight: '10', amount: '300'},
        { index: 4, gname: '洗衣液', unitprice: '30', weight: '10', amount: '300'},
        { index: 1, gname: '茶叶', unitprice: '30', weight: '10', amount: '300'},
        { index: 2, gname: '肥皂', unitprice: '30', weight: '10', amount: '300'},
        { index: 3, gname: '杯子', unitprice: '30', weight: '10', amount: '300'},
        { index: 4, gname: '洗衣液', unitprice: '30', weight: '10', amount: '300'},
        { index: 1, gname: '茶叶', unitprice: '30', weight: '10', amount: '300'},
        { index: 2, gname: '肥皂', unitprice: '30', weight: '10', amount: '300'},
        { index: 3, gname: '杯子', unitprice: '30', weight: '10', amount: '300'},
        { index: 4, gname: '洗衣液', unitprice: '30', weight: '10', amount: '300'},
      ]
    }
  },
  methods: {
    // 分享图片 
    sharePicture({ canvas, path }) {
      const fileData = canvas.toDataURL()
      const fs = uni.getFileSystemManager()
      let that = this
      fs.writeFile({
        filePath: path,
        data: fileData.replace(/^data:image\/\w+;base64,/, ""),
        encoding: 'base64',
        success(res) {
          uni.hideLoading()
          wx.showShareImageMenu({
            withShareTicket: true,
            path: path,
            success: async res => {
              console.log(res);
              uni.navigateBack()
            }
          })
        },
        complete(res) {
          console.log('调用结束');
        }
      })
    },
    // 获取屏幕宽高 
    getSystemInfo(height) {
      let that = this
      uni.getSystemInfo({
        success: function (info) {
          that.canvasWidth = info.windowWidth * info.pixelRatio
          that.canvasHeight = height * info.pixelRatio
        }
      });
    },
    // 绘制canvas 
    toCanvas(){
      uni.showLoading({
        title: '绘制中...',
      })
      let that = this
      arrayInfo = []
      uni.pageScrollTo({
        scrollTop: 0,
        duration: 0
      })
      uni.createSelectorQuery().selectAll('.testClass').boundingClientRect().exec(async (res)=>{
        console.log('获取到信息:',res[0])
        let resArray = res[0]
        await that.getSystemInfo(resArray[0].height)
        // return
        for(let i=0;i<resArray.length;i++){
          const { width, height, left, top } = resArray[i]
          
          if(resArray[i].dataset.type == 'view'){
            console.log('view')
            let radius = resArray[i].dataset.radius
            let bgColor = resArray[i].dataset.bgcolor
            let shadow = resArray[i].dataset.shadow
            let drawRect = resArray[i].dataset.drawRect
            
            arrayInfo.push({
              width, height,
              posX: left,
              posY: top,
              type: 'view',
              radius: radius,
              shadow: shadow ? true : false,
              bgColor: bgColor,
              drawRect: drawRect ? true : false,
            })
          }
          
          else if(resArray[i].dataset.type == 'image'){
            console.log('image')
            let src= resArray[i].dataset.src
            arrayInfo.push({
              width, height,
              posX: left,
              posY: top,
              type:'image',
              src: src,
            })
          }
         
          else if(resArray[i].dataset.type == 'text'){
            console.log('text')
            const { color, word, size, weight } = resArray[i].dataset
            arrayInfo.push({
              color, word, size, weight,
              posX: left,
              posY: top,
              type: 'text',
            })
          }
        }
        // 处理归纳后将数组arrayInfo,传给组件的drawCanvas()方法
        that.$refs.imgCanvas.drawCanvas(arrayInfo)
      })
    },
  }
}

3.css部分

.transpond {
  position: relative;
  color: #333333;
  padding-bottom: 20rpx;

  .position-box {
    position: absolute;
    top: 0;
    height: 323rpx;
    width: 100%;
    background: linear-gradient(180deg, pink 100rpx, #FFFFFF calc(100% - 100rpx));
  }
  .export-but {
    position: fixed;
    bottom: 0;
  }
  .center-box {
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
  }
  .table-box {
    border-radius: 20rpx;
    margin: 20rpx;
    margin-top: 0;
    overflow: hidden;

    .table-title {
      display: flex;
      justify-content: space-between;
      padding: 10rpx 20rpx;
      color: #333;
      background-color: pink;
      width: 100%;
      font-size: 26rpx;
    }
    .table-content {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .table-content {
      padding: 8rpx 20rpx;
      font-size: 24rpx;
      background: #fffcfb;
    }
  }
  .item-box {
    margin: 20rpx;
    margin-top: 0;
    padding: 20rpx;
    position: relative;
    background: #FFFFFF;
    box-shadow: 0rpx 0rpx 14rpx 0rpx rgba(186,186,186,.5);
    border-radius: 20rpx;
    // border-bottom: 10rpx solid #dedede;
    // margin-bottom: 20rpx;
  }

  .title-info {
    padding-bottom: 5rpx;
    font-size: 34rpx;
    font-weight: bold;
    padding-bottom: 20rpx;
  }
  .user-info {
    position: relative;
    display: flex;
    font-size: 28rpx;
    .tag {
      text-align: center;
      padding: 0 10rpx;
      // height: 50rpx;
      background: #FFC749;
      border-radius: 12rpx;
    }
    &-item {
      display: flex;
      padding-bottom: 16rpx;

      view:first-child {
        font-weight: bold;
      }
    }
  }
  .icon-box {
    position: absolute;
    top: 0;
    right: 0;
    width: 179rpx;
    height: 200rpx;
    background: pink;
    border-radius: 18rpx;
    color: #FFFFFF;
    display: flex;
    align-items: center;
    justify-content: center;
    .number-text {
      font-weight: bold;
      font-size: 49rpx;
      text-align: center;
    }
    .number-info {
      font-weight: 500;
      font-size: 26rpx;
      text-align: center;
    }
  }
  .title-name {
    padding: 56rpx 0;
    position: relative;
    text-align: center;
    width: 100%;
    font-weight: 600;
    font-size: 37rpx;
  }
  .title-date {
    padding-bottom: 20rpx;
    position: relative;
    text-align: center;
    width: 100%;
    font-weight: bold;
    font-size: 24rpx;
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容