接到一个需求是用户要截长屏并转发到工作群,但是微信小程序获取不到dom,于是用canvas绘制页面后导出。
效果
VeryCapture_20240819150308.gif
实现过程:
- 根据页面内容设置canvas画布的框高
- js获取html标签中的信息(标签类型,字号,字重,颜色,背景等),整理并传递给封装的canvas组件
- canvas组件获取信息将内容绘制到画布上
- 将画布导出为图片
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;
}
}