接到需求开发一款电子名片小程序,前期将技术栈设为了Taro + React,在开发过程中,电子名片前期由原生标签制作,但显示效果差强人意,而且分享名片时,只能通过微信官方的分享将名片网页截取一部分,会将无关内容带到分享页中。因此,我将名片改为canvas实现,分享时将canvas转为临时图片,达到小程序的4:5比例。
lib文件夹文件可到以下链接获取。
https://github.com/Lsq-class/tuoersuo_game_cal/tree/main/lib
tsximport Taro from "@tarojs/taro";
import { Canvas } from "@tarojs/components";
import Pen from "./lib/pen";
import Downloader from "./lib/downloader";
import { getAuthSetting, saveImageToPhotosAlbum, equal } from "./lib/util";
import React from "react";
const downloader = new Downloader();
// 最大尝试的绘制次数
const MAX_PAINT_COUNT = 5;
interface IProps {
customStyle: string; // canvas自定义样式
palette: object; // painter模板
widthPixels: number; // 像素宽度
dirty: boolean; // 启用脏检查,默认 false
onImgErr: Function; // 图片失败回调
onImgOK: Function; // 图片成功回调
}
interface IState {
painterStyle: string; // canvas 宽度+高度样式
}
export default class QyPoster extends React.Component<IProps, IState> {
static defaultProps = {
customStyle: "",
palette: {},
widthPixels: 0,
dirty: false,
onImgErr: () => null,
onImgOK: () => null
};
canvasId: string = "k-canvas"; // canvas-id
filePath: string = ''; // 生成的文件路径
state: IState = {
painterStyle: ""
};
canvasWidthInPx: number = 0; // width to px
canvasHeightInPx: number = 0; // height to px
paintCount: number = 0; // 绘制次数
/**
* 判断一个 object 是否为空
* @param {object} object
*/
isEmpty(object) {
for (const _i in object) {
return false;
}
return true;
}
isNeedRefresh = (newVal, oldVal) => {
if (
!newVal ||
this.isEmpty(newVal) ||
(this.props.dirty && equal(newVal, oldVal))
) {
return false;
}
return true;
};
setStringPrototype = (screenK, scale) => {
/**
* 是否支持负数
* @param {Boolean} minus 是否支持负数
*/
//@ts-ignore
String.prototype.toPx = function toPx(minus) {
let reg;
if (minus) {
reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;
} else {
reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;
}
const results = reg.exec(this);
if (!this || !results) {
console.error(`The size: ${this} is illegal`);
return 0;
}
const unit = results[2];
const value = parseFloat(this);
let res = 0;
if (unit === "rpx") {
res = Math.round(value * screenK * (scale || 1));
} else if (unit === "px") {
res = Math.round(value * (scale || 1));
}
return res;
};
};
// 执行绘制
startPaint = (palette) => {
// 如果palette模板为空 则return
if (this.isEmpty(palette)) {
return;
}
if (!(Taro.getApp().systemInfo && Taro.getApp().systemInfo.screenWidth)) {
try {
Taro.getApp().systemInfo = Taro.getSystemInfoSync();
} catch (e) {
const error = `Painter get system info failed, ${JSON.stringify(e)}`;
console.error(error);
this.props.onImgErr && this.props.onImgErr(error);
return;
}
}
let screenK = Taro.getApp().systemInfo.screenWidth / 750;
this.setStringPrototype(screenK, 1);
this.downloadImages(palette).then((palette: any) => {
const { width, height } = palette;
if (!width || !height) {
console.error(
`You should set width and height correctly for painter, width: ${width}, height: ${height}`
);
return;
}
this.canvasWidthInPx = width.toPx();
if (this.props.widthPixels) {
// 如果重新设置过像素宽度,则重新设置比例
this.setStringPrototype(
screenK,
this.props.widthPixels / this.canvasWidthInPx
);
this.canvasWidthInPx = this.props.widthPixels;
}
this.canvasHeightInPx = height.toPx();
this.setState({
painterStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`
});
const ctx = Taro.createCanvasContext(this.canvasId, this.$scope);
const pen = new Pen(ctx, palette);
pen.paint(() => {
this.saveImgToLocal();
});
});
};
// 下载图片
downloadImages = (palette) => {
return new Promise(resolve => {
let preCount = 0;
let completeCount = 0;
const paletteCopy = JSON.parse(JSON.stringify(palette));
if (paletteCopy.background) {
preCount++;
downloader.download(paletteCopy.background).then(
path => {
paletteCopy.background = path;
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
() => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}
);
}
if (paletteCopy.views) {
for (const view of paletteCopy.views) {
if (view && view.type === "image" && view.url) {
preCount++;
downloader.download(view.url).then(
path => {
view.url = path;
Taro.getImageInfo({
src: view.url,
//@ts-ignore
success: res => {
// 获得一下图片信息,供后续裁减使用
view.sWidth = res.width;
view.sHeight = res.height;
},
fail: error => {
// 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
view.url = "";
console.error(
`getImageInfo ${view.url} failed, ${JSON.stringify(
error
)}`
);
},
complete: () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}
});
},
() => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}
);
}
}
}
if (preCount === 0) {
resolve(paletteCopy);
}
});
};
// 保存图片到本地
saveImgToLocal = () => {
setTimeout(() => {
Taro.canvasToTempFilePath(
{
canvasId: this.canvasId,
success: res => {
this.getImageInfo(res.tempFilePath);
},
fail: error => {
console.error(
`canvasToTempFilePath failed, ${JSON.stringify(error)}`
);
this.props.onImgErr && this.props.onImgErr(error);
}
},
this.$scope
);
}, 300);
};
getImageInfo = filePath => {
Taro.getImageInfo({
src: filePath,
//@ts-ignore
success: infoRes => {
if (this.paintCount > MAX_PAINT_COUNT) {
const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
console.error(error);
this.props.onImgErr && this.props.onImgErr(error);
return;
}
// 比例相符时才证明绘制成功,否则进行强制重绘制
if (
Math.abs(
(infoRes.width * this.canvasHeightInPx -
this.canvasWidthInPx * infoRes.height) /
(infoRes.height * this.canvasHeightInPx)
) < 0.01
) {
this.filePath = filePath;
this.props.onImgOK && this.props.onImgOK({ path: filePath });
} else {
this.startPaint(this.props.palette);
}
this.paintCount++;
},
fail: error => {
console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
this.props.onImgErr && this.props.onImgErr(error);
}
});
};
// 保存海报到手机相册
saveImage() {
const scope = "scope.writePhotosAlbum";
getAuthSetting(scope).then((res: boolean) => {
if (res) {
// 授权过 直接保存
this.saveImageToPhotos();
return false;
}
// 未授权过 先获取权限
getAuthSetting(scope).then((status: boolean) => {
if (status) {
// 获取保存图片到相册权限成功
this.saveImageToPhotos();
return false;
}
// 用户拒绝授权后的回调 获取权限失败
Taro.showModal({
title: "提示",
content: "若不打开授权,则无法将图片保存在相册中!",
showCancel: true,
cancelText: "暂不授权",
cancelColor: "#000000",
confirmText: "去授权",
confirmColor: "#3CC51F",
success: function (res) {
if (res.confirm) {
// 用户点击去授权
Taro.openSetting({
//调起客户端小程序设置界面,返回用户设置的操作结果。
});
} else {
//
}
}
});
});
});
}
getImportUrl() {
return this.filePath
}
saveImageToPhotos = () => {
saveImageToPhotosAlbum(this.filePath)
.then(() => {
// 成功保存图片到本地相册
// 保存失败
Taro.showToast({
title: "保存成功",
icon: "none"
});
})
.catch(() => {
// 保存失败
Taro.showToast({
title: "保存失败",
icon: "none"
});
});
};
componentWillMount() {
this.startPaint(this.props.palette);
}
componentWillReceiveProps(nextProps) {
if (nextProps.palette !== this.props.palette) {
this.paintCount = 0;
this.startPaint(nextProps.palette);
}
}
render() {
return (
<Canvas
canvasId={this.canvasId}
style={`${this.state.painterStyle}${this.props.customStyle}`}
/>
);
}
}
tsximport Taro, { Config } from "@tarojs/taro";
import { View, Button } from "@tarojs/components";
import Card from "./card";
import Poster from "../poster";
import "./index.scss";
// eslint-disable-next-line import/first
import { Component } from "react";
// eslint-disable-next-line import/first
import { deepEqual } from "~/servers/methods/utils";
// 名片内字段定义
export interface CardIProps {
avatarUrl,
name,
jobTitle,
company,
location,
email,
phone,
BackgroundUrl,
background,
isVisibel?: boolean,
}
interface IState {
imagePath: string;
template: object;
}
export default class Index extends Component<CardIProps, IState> {
/**
* 指定config的类型声明为: Taro.Config
*
* 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
* 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
* 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
*/
config: Config = {
navigationBarTitleText: "painter"
};
state: IState = {
template: new Card().palette(),
// eslint-disable-next-line react/no-unused-state
imagePath: "",
};
// 图片生成成功回调
onImgOK = e => {
this.setState({
// eslint-disable-next-line react/no-unused-state
imagePath: e.path
});
};
// 图片生成失败回调
onImgErr = error => {
console.log(
"%cerror: ",
"color: MidnightBlue; background: Aquamarine; font-size: 20px;",
error
);
};
public painterRef: Poster | null;
// 保存图片到本地相册
getImageUrl() {
if (this.painterRef) {
return this.painterRef.getImportUrl()
}
}
componentWillReceiveProps(nextProps: Readonly<CardIProps>, nextContext: any): void {
if (!deepEqual(nextProps, this.props)) {
this.setState({ template: new Card().palette({ ...nextProps }) })
}
}
render() {
const { isVisibel } = this.props
return (
<View className="index">
<Poster
customStyle={isVisibel ? "display: none;" : ""}
palette={this.state.template}
onImgOK={this.onImgOK}
onImgErr={this.onImgErr}
ref={node => {
this.painterRef = node
}}
dirty={false}
/>
</View>
);
}
}
由于需要不同背景,canvas展示位置不同,因此这里对样式做了特殊判断
tsximport { CardIProps } from "./ExportImg"
export default class LastMayday {
palette(cardProps?: CardIProps) {
const isBlueBack = cardProps?.background === 1 || cardProps?.background === 4
const isOneStyle = cardProps?.background === 3 || cardProps?.background === 5 || cardProps?.background === 1 || cardProps?.background === 4
const fontColor = {
color: isBlueBack ? '#FFFFFF' : undefined
};
const infoFontColor = {
color: isBlueBack ? '#d4eafe' : undefined
};
const infoPosition = {
left: isOneStyle ? "40rpx" : '220rpx'
}
const avatarPosition = {
left: isOneStyle ? "" : '40rpx',
right: isOneStyle && '40rpx'
}
const textPositon = {
left: isOneStyle ? "82rpx" : '262rpx'
}
return (
{
"width": "702rpx",
"height": "484rpx",
"background": "链接地址",
borderRadius: '24rpx',
border: "4rpx solid #F4F4F4",
backgroundSize: 'cover',
"views": [
{
type: 'text',
text: cardProps?.company ? cardProps?.company : "",
css: [{
top: `44rpx`,
fontWeight: 'bold',
color: '#262832',
fontSize: '32rpx',
left: '40rpx'
}, fontColor],
},
{
"type": "image",
"url": cardProps?.avatarUrl ? cardProps?.avatarUrl : "",
"css": [{
"width": "140rpx",
"height": "140rpx",
"top": "110rpx",
"rotate": "0",
"borderRadius": "70rpx",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "aspectFill"
}, avatarPosition]
},
{
type: 'text',
text: cardProps?.name ? cardProps?.name : "",
css: [{
top: `121rpx`,
fontWeight: 'bold',
fontSize: '44rpx',
color: '#262832'
}, fontColor, infoPosition],
}, {
type: 'text',
text: cardProps?.jobTitle ? cardProps?.jobTitle : "",
css: [{
top: `195rpx`,
// fontWeight: 'bold',
fontSize: '30rpx',
color: '#262832'
}, fontColor, infoPosition],
},
{
"type": "image",
"url": "链接地址",
"css": [{
"width": "24rpx",
"height": "24rpx",
"top": "291rpx",
"rotate": "0",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "aspectFill"
}, infoPosition]
},
{
type: 'text',
text: cardProps?.phone ? cardProps?.phone : "",
css: [{
top: `282rpx`,
// left: '242rpx',
fontSize: '28rpx',
color: '#17171A'
}, infoFontColor, textPositon],
}, {
"type": "image",
"url": "链接地址",
"css": [{
"width": "24rpx",
"height": "24rpx",
"top": "339rpx",
"rotate": "0",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "aspectFill"
}, infoPosition]
},
{
type: 'text',
text: cardProps?.email ? cardProps?.email : "",
css: [{
top: `330rpx`,
// left: '242rpx',
fontSize: '28rpx',
color: '#17171A'
}, infoFontColor, textPositon],
}, {
"type": "image",
"url": "链接地址",
"css": [{
"width": "24rpx",
"height": "24rpx",
"top": "387rpx",
"rotate": "0",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "aspectFill"
}, infoPosition]
},
{
type: 'text',
text: cardProps?.location ? cardProps?.location : "",
css: [{
top: `378rpx`,
// left: '242rpx',
fontSize: '28rpx',
lineHeight: '38rpx',
color: '#17171A',
width: isOneStyle ? "580rpx" :'420rpx'
}, infoFontColor, textPositon],
},
]
}
);
}
}
tsx{
"width": "702rpx", //canvas宽度
"height": "484rpx", // canvas高度
"background": "链接地址", // 背景图
borderRadius: '24rpx', // 边框圆角
border: "4rpx solid #F4F4F4", // 边框
backgroundSize: 'cover', // 背景设置覆盖方式
"views": [ // 配置canvas内子元素
{
type: 'text', // 文字类型
text: "lsq_137", // 文字内容
css: [{ // 给文字设置样式(支持多对象插入)
top: `44rpx`,
fontWeight: 'bold',
color: '#262832',
fontSize: '32rpx',
left: '40rpx'
}],
},
{
"type": "image", // 图片类型
"url": "图片链接地址", // 链接地址
"css": [{ // 样式(支持多对象插入)
"width": "140rpx",
"height": "140rpx",
"top": "110rpx",
"rotate": "0",
"borderRadius": "70rpx",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "aspectFill"
}, {color: "#fff"}
]
}
]
tsximport { useEffect, useRef, useState } from "react";
import { View, Image, Canvas } from "@tarojs/components";
import { Icon } from "@antmjs/vantui";
import Taro, {
useShareAppMessage,
} from "@tarojs/taro";
function Index() {
const [cardInfo, cardInfoSet] = useState<any>();
let painterRef: any = useRef(null)
const onInit = async () => {
};
/**
* 生成分享5:4的图片
*/
const makeCanvas = (imgUrl) => {
return new Promise((resolve, reject) => {
// 获取图片信息,小程序下获取网络图片信息需先配置download域名白名单才能生效
const sysInfo = Taro.getSystemInfoSync()
Taro.getImageInfo({
src: imgUrl,
success: (imgInfo) => {
// 获取设备像素比 适配图片
const pixelRatio = sysInfo.pixelRatio
let ctx = Taro.createCanvasContext('canvas')
let canvasW = imgInfo.width / pixelRatio
let canvasH = ((imgInfo.width * 4) / 5) / pixelRatio
// 把比例设置为 宽比高 5:4
// canvasW = (imgInfo.height * 5) / 4
// 为画框设置背景色,注意要放在画图前,图会覆盖在背景色上
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvasW, canvasH)
ctx.drawImage(
imgInfo.path,
0,
(canvasH - imgInfo.height / pixelRatio) / 2,
canvasW,
imgInfo.height / pixelRatio
)
// }
ctx.draw(false, () => {
setTimeout(() => {
Taro.canvasToTempFilePath({
width: canvasW,
height: canvasH,
// destWidth: 750, // 标准的iphone6尺寸的两倍,生成高清图
// destHeight: 600,
canvasId: "canvas",
fileType: "png", // 注意jpg默认背景为透明
success: (res) => {
resolve(res.tempFilePath)
},
fail: (err) => {
reject(err)
}
})
}, 0)
})
},
fail: (err) => {
reject(err)
}
})
})
}
useShareAppMessage((res) => {
if (res.from === "button") {
// 来自页面内转发按钮
console.log(res.target);
}
let imgUrl = ""
if (painterRef) {
imgUrl = painterRef?.getImageUrl()
}
return new Promise((resolve, reject) => {
return makeCanvas(imgUrl).then(imgPath => {
resolve({
title: "请保存!",
path: `/pages/mineHome/index?open_code=${cardInfo?.open_code}&visit_from=1`,
imageUrl: imgPath
})
}).catch(err => {
resolve({
title: "请保存!",
path: `/pages/mineHome/index?open_code=${cardInfo?.open_code}&visit_from=1`,
// imageUrl: '处理失败后展示的图片,可以用原图shareMessage.imageUrl'
})
})
})
});
const cardInfoProps = {
name: cardInfo?.name,
company: cardInfo?.company,
jobTitle: cardInfo?.job?.[0],
phone: cardInfo?.cellphone,
email: cardInfo?.email?.[0],
location: cardInfo?.address,
avatarUrl:
cardInfo?.avatar,
BackgroundUrl:
cardInfo?.background_picture,
background: cardInfo?.background
}
// 获取当前设备屏幕宽度
const sysInfo = Taro.getSystemInfoSync()
const canvasHeight = (sysInfo?.screenWidth * 4) / 5
return (
<View className="page">
<View id="export-element">
<ExportImg ref={node => {
painterRef = node
}}
isVisibel={isVisit}
{...cardInfoProps}
/>
</View>
{* 导出图片使用到的canvas *}
<Canvas
canvasId={"canvas"}
style={`position: absolute; top: -1000px; left: -1000px; width: ${sysInfo?.screenWidth}px; height: ${canvasHeight}px;`}
/>
</View>
);
}
export default Index;
本文作者:lsq_137
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!