123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- // Canvas工具类 - 用于处理海报生成
- export class CanvasUtils {
- constructor(canvasId) {
- this.canvasId = canvasId;
- }
- // 下载图片到本地(带重试机制)
- downloadImage(src, retryCount = 3) {
- return new Promise((resolve, reject) => {
-
- // 如果是本地路径,直接返回
- if (src.startsWith('/') || src.startsWith('http://localhost') || src.startsWith('file://')) {
- resolve(src);
- return;
- }
-
- const tryDownload = (attempt = 1) => {
- uni.downloadFile({
- url: src,
- success: (res) => {
- if (res.statusCode === 200) {
- resolve(res.tempFilePath);
- } else {
- console.error(`图片下载失败,状态码: ${res.statusCode}`);
- if (attempt < retryCount) {
- setTimeout(() => {
- tryDownload(attempt + 1);
- }, 1000 * attempt);
- } else {
- reject(new Error(`下载失败,状态码: ${res.statusCode}`));
- }
- }
- },
- fail: (err) => {
- console.error(`图片下载失败 (第${attempt}次): ${src}`, err);
- if (attempt < retryCount) {
- setTimeout(() => {
- tryDownload(attempt + 1);
- }, 1000 * attempt);
- } else {
- console.error(`图片下载最终失败: ${src}`);
- reject(err);
- }
- }
- });
- };
- tryDownload();
- });
- }
- // 绘制海报
- drawPoster(data) {
- return new Promise(async (resolve, reject) => {
- try {
- // 基础参数 - 使用2倍分辨率提高清晰度
- const w = uni.upx2px(690) * 2
- const top = uni.upx2px(0) * 2
- const r = uni.upx2px(24) * 2
-
- // 计算内容布局参数 - 使用2倍分辨率
- const watermarkArr = data.treeObj.watermarkArr || [];
- const lineHeight = uni.upx2px(40) * 2; // 水印文字行间距
- const startY = uni.upx2px(870) * 2; // 水印文字起始Y坐标
- const lastWatermarkY = startY + ((watermarkArr.length - 1) * lineHeight);
-
- const logoHeight = uni.upx2px(80) * 2;
- const logoSpacing = uni.upx2px(26) * 2;
- const logoY = lastWatermarkY - logoHeight - logoSpacing;
- const brandTextY = logoY + logoHeight + logoSpacing;
-
- // 使用固定高度,确保有足够空间显示所有内容
- // 计算canvas高度(根据内容自适应)
- const bottomMargin = uni.upx2px(20) * 2; // 底部边距
- const h = brandTextY + uni.upx2px(30) * 2 + bottomMargin; // 30rpx是文字高度估算
-
-
- // 下载所有图片到本地
- const imagePromises = [];
- const downloadedImages = {};
-
- // 下载二维码图片
- if (data.treeObj.qrCodeUrl) {
- imagePromises.push(
- this.downloadImage(data.treeObj.qrCodeUrl).then(localPath => {
- downloadedImages.qrCodeUrl = localPath;
- })
- );
- }
-
- // 下载树图片
- if (data.treeObj.posterUrl) {
- imagePromises.push(
- this.downloadImage(data.treeObj.posterUrl).then(localPath => {
- downloadedImages.posterUrl = localPath;
- })
- );
- }
-
- // 下载树牌背景图片
- const treeNameBgUrl = 'https://birdseye-img.sysuimars.com/youwei-uniapp/img/treePage/tag-bg.png';
- imagePromises.push(
- this.downloadImage(treeNameBgUrl).then(localPath => {
- downloadedImages.treeNameBgUrl = localPath;
- })
- );
-
- // 下载logo图片
- const logoUrl = 'https://birdseye-img.sysuimars.com/youwei-uniapp/img/treePage/logo.png';
- imagePromises.push(
- this.downloadImage(logoUrl).then(localPath => {
- downloadedImages.logoUrl = localPath;
- })
- );
-
- // 等待所有图片下载完成(带容错处理)
- try {
- await Promise.all(imagePromises);
- } catch (error) {
- // 继续执行,让canvas尝试绘制可用的图片
- }
-
- const ctx = uni.createCanvasContext(this.canvasId);
-
- if (!ctx) {
- reject(new Error('canvas context创建失败'));
- return;
- }
-
- // 清空canvas
- ctx.clearRect(0, 0, w, h);
-
- // 绘制白色背景
- ctx.setFillStyle('rgba(255, 255, 255, 0)')
- ctx.fillRect(0, 0, w, h);
- this.drawRoundedRect(ctx, 0, top, w, h, r, '#ffffff');
- // 绘制年份
- ctx.setFillStyle('#000000')
- ctx.setTextAlign('left') // 设置文字对齐方式
- ctx.setTextBaseline('top') // 设置文字基线
- ctx.font = `bold ${uni.upx2px(36) * 2}px "SweiSpringCJKtc", Arial, sans-serif` // 设置字体样式:粗体 + 自定义字体
- ctx.fillText(data.treeObj.year, uni.upx2px(26) * 2, uni.upx2px(34) * 2)
- // 绘制月份
- ctx.font = `bold ${uni.upx2px(172) * 2}px "SweiSpringCJKtc", Arial, sans-serif` // 设置字体样式:粗体 + 自定义字体,172rpx大小
- ctx.fillText(data.treeObj.monthNumber, uni.upx2px(26) * 2, uni.upx2px(76) * 2)
-
- // 绘制斜线
- ctx.setLineWidth(uni.upx2px(4) * 2) // 设置线条宽度
- ctx.beginPath()
- ctx.moveTo(uni.upx2px(160) * 2, uni.upx2px(180) * 2) // 斜线起点(右上角)
- ctx.lineTo(uni.upx2px(140) * 2, uni.upx2px(220) * 2) // 斜线终点(左下角)
- ctx.stroke()
-
- // 绘制日
- ctx.font = `bold ${uni.upx2px(48) * 2}px "SweiSpringCJKtc", Arial, sans-serif` // 设置字体样式:粗体 + 自定义字体,172rpx大小
- ctx.fillText(data.treeObj.day, uni.upx2px(162) * 2, uni.upx2px(180) * 2)
- // 绘制二维码图片
- if (downloadedImages.qrCodeUrl) {
- try {
- ctx.drawImage(downloadedImages.qrCodeUrl, uni.upx2px(545) * 2, uni.upx2px(34) * 2, uni.upx2px(130) * 2, uni.upx2px(142) * 2);
- } catch (error) {
- }
- }
- // 添加品种 绘制树龄 - 靠右对齐
- ctx.setFontSize(uni.upx2px(24) * 2)
- ctx.setTextAlign('right')
- ctx.fillText(`${data.treeObj.pz} ${data.treeObj.age}年 ${data.treeObj.age > 9 ? "老树" : "树龄"}`, uni.upx2px(665) * 2, uni.upx2px(186) * 2)
-
- // 绘制气候适宜 绘制采摘方式 - 靠右对齐
- ctx.setFontSize(uni.upx2px(24) * 2)
- ctx.setTextAlign('right')
- ctx.fillText(`${data.treeObj.phenology} 气候适宜-${data.treeObj.howTxt || "果园采摘"}`, uni.upx2px(665) * 2, uni.upx2px(225) * 2)
- // 绘制树图片(带圆角)
- const imgX = uni.upx2px(26) * 2;
- const imgY = uni.upx2px(280) * 2;
- const imgW = uni.upx2px(640) * 2;
- const imgH = uni.upx2px(480) * 2;
- const radius = uni.upx2px(10) * 2;
-
- // 创建圆角路径
- ctx.beginPath();
- ctx.moveTo(imgX + radius, imgY);
- ctx.lineTo(imgX + imgW - radius, imgY);
- ctx.arc(imgX + imgW - radius, imgY + radius, radius, -Math.PI/2, 0);
- ctx.lineTo(imgX + imgW, imgY + imgH - radius);
- ctx.arc(imgX + imgW - radius, imgY + imgH - radius, radius, 0, Math.PI/2);
- ctx.lineTo(imgX + radius, imgY + imgH);
- ctx.arc(imgX + radius, imgY + imgH - radius, radius, Math.PI/2, Math.PI);
- ctx.lineTo(imgX, imgY + radius);
- ctx.arc(imgX + radius, imgY + radius, radius, Math.PI, -Math.PI/2);
- ctx.closePath();
-
- // 裁剪为圆角区域
- ctx.save();
- ctx.clip();
-
- // 绘制图片
- if (downloadedImages.posterUrl) {
- try {
- ctx.drawImage(downloadedImages.posterUrl, imgX, imgY, imgW, imgH);
- } catch (error) {
- console.error('树图片绘制失败:', error);
- }
- }
-
- // 恢复裁剪
- ctx.restore();
- // 绘制图片文字背景和边框(圆角)
- const textX = uni.upx2px(450) * 2;
- const textY = uni.upx2px(316) * 2;
- const textWidth = uni.upx2px(180) * 2; // 背景宽度
- const textHeight = uni.upx2px(18) * 2; // 背景高度
- const padding = uni.upx2px(16) * 2; // 内边距
- const textRadius = uni.upx2px(25) * 2; // 圆角半径
-
- // 绘制黑色半透明背景(圆角)
- ctx.setFillStyle('rgba(0, 0, 0, 0.6)');
- ctx.beginPath();
- ctx.moveTo(textX - padding + textRadius, textY - padding);
- ctx.lineTo(textX - padding + textWidth + padding * 2 - textRadius, textY - padding);
- ctx.arc(textX - padding + textWidth + padding * 2 - textRadius, textY - padding + textRadius, textRadius, -Math.PI/2, 0);
- ctx.lineTo(textX - padding + textWidth + padding * 2, textY - padding + textHeight + padding * 2 - textRadius);
- ctx.arc(textX - padding + textWidth + padding * 2 - textRadius, textY - padding + textHeight + padding * 2 - textRadius, textRadius, 0, Math.PI/2);
- ctx.lineTo(textX - padding + textRadius, textY - padding + textHeight + padding * 2);
- ctx.arc(textX - padding + textRadius, textY - padding + textHeight + padding * 2 - textRadius, textRadius, Math.PI/2, Math.PI);
- ctx.lineTo(textX - padding, textY - padding + textRadius);
- ctx.arc(textX - padding + textRadius, textY - padding + textRadius, textRadius, Math.PI, -Math.PI/2);
- ctx.closePath();
- ctx.fill();
-
- // 绘制白色边框(圆角)
- ctx.setStrokeStyle('rgba(255, 255, 255, 0.39)');
- ctx.setLineWidth(uni.upx2px(2) * 2);
- ctx.beginPath();
- ctx.moveTo(textX - padding + textRadius, textY - padding);
- ctx.lineTo(textX - padding + textWidth + padding * 2 - textRadius, textY - padding);
- ctx.arc(textX - padding + textWidth + padding * 2 - textRadius, textY - padding + textRadius, textRadius, -Math.PI/2, 0);
- ctx.lineTo(textX - padding + textWidth + padding * 2, textY - padding + textHeight + padding * 2 - textRadius);
- ctx.arc(textX - padding + textWidth + padding * 2 - textRadius, textY - padding + textHeight + padding * 2 - textRadius, textRadius, 0, Math.PI/2);
- ctx.lineTo(textX - padding + textRadius, textY - padding + textHeight + padding * 2);
- ctx.arc(textX - padding + textRadius, textY - padding + textHeight + padding * 2 - textRadius, textRadius, Math.PI/2, Math.PI);
- ctx.lineTo(textX - padding, textY - padding + textRadius);
- ctx.arc(textX - padding + textRadius, textY - padding + textRadius, textRadius, Math.PI, -Math.PI/2);
- ctx.closePath();
- ctx.stroke();
-
- // 绘制文字
- ctx.setFillStyle('#ffffff');
- ctx.setTextAlign('center');
- ctx.setTextBaseline('middle');
- // 重置字体为默认字体
- ctx.font = `bold ${uni.upx2px(20) * 2}px Arial, sans-serif`;
- ctx.fillText(`${data.treeObj.pz}-${data.treeObj.countyName}`, textX + textWidth / 2, textY + textHeight / 2);
-
- // 绘制树牌文字背景
- if (downloadedImages.treeNameBgUrl) {
- try {
- ctx.drawImage(downloadedImages.treeNameBgUrl, uni.upx2px(40) * 2, uni.upx2px(486) * 2, uni.upx2px(276) * 2, uni.upx2px(274) * 2);
- } catch (error) {
- console.error('树牌背景图片绘制失败:', error);
- }
- }
- // 绘制树牌文字
- ctx.setFillStyle('#ffffff');
- ctx.font = `bold ${uni.upx2px(36) * 2}px "jiangxizhuokai", Arial, sans-serif`
- ctx.fillText(`【${data.treeName}】`, uni.upx2px(178) * 2, uni.upx2px(565) * 2)
- ctx.font = `bold ${uni.upx2px(18) * 2}px "jiangxizhuokai", Arial, sans-serif`
- ctx.fillText(`${data.userInfo.nickname || data.userInfo.name} ${data.treeObj.year}.${data.treeObj.monthNumber >= 10 ? data.treeObj.monthNumber : "0" + data.treeObj.monthNumber}.${data.treeObj.day}`, uni.upx2px(178) * 2, uni.upx2px(610) * 2)
-
- // 绘制横线(在水印文字上面)
- ctx.setStrokeStyle('#000000'); // 设置线条颜色为黑色
- ctx.setLineWidth(uni.upx2px(2) * 2);
-
- // 计算第一行水印文字的长度
- const firstWatermarkText = data.treeObj.watermarkArr[0] || '';
- ctx.font = `bold ${uni.upx2px(24) * 2}px "SweiSpringCJKtc", Arial, sans-serif`;
- const textMetrics = ctx.measureText(firstWatermarkText);
- const watermarkTextWidth = textMetrics.width;
-
- ctx.beginPath();
- ctx.moveTo(uni.upx2px(36) * 2, uni.upx2px(820) * 2); // 横线起点,左边距离36rpx
- ctx.lineTo(uni.upx2px(36) * 2 + watermarkTextWidth, uni.upx2px(820) * 2); // 横线终点,根据文字长度
- ctx.stroke();
- // 绘制水印文字(根据数组长度动态生成)
- ctx.setFillStyle('#000000');
- ctx.font = `bold ${uni.upx2px(24) * 2}px "SweiSpringCJKtc", Arial, sans-serif`
- ctx.setTextAlign('left'); // 设置左对齐,确保第一个字在指定位置
-
- watermarkArr.forEach((text, index) => {
- const y = startY + (index * lineHeight);
- ctx.fillText(text, uni.upx2px(36) * 2, y);
- });
- // 绘制logo(与最后一条水印文字对齐)
- if (downloadedImages.logoUrl) {
- try {
- ctx.drawImage(downloadedImages.logoUrl, uni.upx2px(590) * 2, logoY, uni.upx2px(76) * 2, logoHeight);
- } catch (error) {
- console.error('logo图片绘制失败:', error);
- }
- }
- // 绘制飞鸟有味(与logo保持间距)
- ctx.setFillStyle('#000000');
- ctx.font = `bold ${uni.upx2px(22) * 2}px "SweiSpringCJKtc", Arial, sans-serif`
- ctx.fillText(`飞鸟有味`, uni.upx2px(580) * 2, brandTextY);
- ctx.draw(false, () => {
- // 等待canvas渲染完成
- setTimeout(() => {
- // 额外等待时间确保图片完全渲染
- setTimeout(() => {
- resolve();
- }, 1000);
- }, 2000); // 增加等待时间确保图片渲染完成
- });
-
- } catch (error) {
- console.error('canvas绘制出错:', error);
- reject(error);
- }
- });
- }
-
- // 绘制圆角矩形
- drawRoundedRect(ctx, x, y, w, h, r, color) {
- ctx.setFillStyle(color);
- ctx.beginPath();
- // 左上角
- ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
- // 右上角
- ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
- // 右下角
- 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)
- ctx.closePath()
- ctx.fill()
- }
- // 保存到相册
- saveToAlbum() {
- return new Promise((resolve, reject) => {
- uni.showLoading({
- title: '正在生成海报...',
- mask: true
- });
-
- // 先检查canvas是否存在
- const query = uni.createSelectorQuery();
- query.select(`#${this.canvasId}`).boundingClientRect((rect) => {
- if (!rect) {
- console.error('Canvas元素不存在');
- uni.hideLoading();
- uni.showToast({
- title: 'Canvas元素不存在',
- icon: 'none'
- });
- reject(new Error('Canvas元素不存在'));
- return;
- }
-
-
- uni.canvasToTempFilePath({
- canvasId: this.canvasId,
- width: 1380, // 2倍分辨率宽度
- height: 2280, // 2倍分辨率高度
- destWidth: 1380, // 2倍分辨率宽度
- destHeight: 2280, // 2倍分辨率高度
- fileType: 'png',
- success: (res) => {
- uni.hideLoading();
-
- uni.showLoading({
- title: '正在保存...',
- mask: true
- });
-
- // 生成带时间戳的海报文件名
- const timestamp = new Date().getTime();
- const posterFileName = `海报_${timestamp}.png`;
-
- uni.saveImageToPhotosAlbum({
- filePath: res.tempFilePath,
- success: () => {
- uni.hideLoading();
- uni.showToast({
- title: '海报保存成功',
- icon: 'success',
- duration: 2000
- });
- resolve(true);
- },
- fail: (err) => {
- console.error('保存到相册失败:', err);
- uni.hideLoading();
-
- if (err.errMsg && err.errMsg.includes('auth deny')) {
- uni.showModal({
- title: '权限提示',
- content: '需要相册权限才能保存图片,请前往设置开启',
- confirmText: '去设置',
- success: (modalRes) => {
- if (modalRes.confirm) {
- uni.openSetting();
- }
- }
- });
- } else {
- uni.showToast({
- title: '保存失败: ' + (err.errMsg || '未知错误'),
- icon: 'none',
- duration: 3000
- });
- }
- reject(err);
- }
- });
- },
- fail: (err) => {
- console.error('canvas转图片失败:', err);
- uni.hideLoading();
- uni.showToast({
- title: '图片生成失败: ' + (err.errMsg || '未知错误'),
- icon: 'none',
- duration: 3000
- });
- reject(err);
- }
- });
- }).exec();
- });
- }
- // 完整的保存流程
- async generateAndSave(data) {
- try {
-
- // 检查数据完整性
- if (!data || !data.treeObj) {
- throw new Error('数据不完整');
- }
-
- // 检查相册权限
- const authResult = await this.checkPhotoAlbumAuth();
- if (!authResult) {
- throw new Error('没有相册权限');
- }
-
- await this.drawPoster(data);
-
- await this.saveToAlbum();
-
- return true;
- } catch (error) {
- console.error('海报生成和保存失败:', error);
- uni.showToast({
- title: error.message || '海报保存失败',
- icon: 'none'
- });
- return false;
- }
- }
- // 检查相册权限
- checkPhotoAlbumAuth() {
- return new Promise((resolve) => {
- uni.getSetting({
- success: (res) => {
- if (res.authSetting['scope.writePhotosAlbum'] === false) {
- // 用户之前拒绝了权限,引导用户开启
- uni.showModal({
- title: '提示',
- content: '需要相册权限才能保存图片,是否前往设置?',
- success: (modalRes) => {
- if (modalRes.confirm) {
- uni.openSetting({
- success: (settingRes) => {
- if (settingRes.authSetting['scope.writePhotosAlbum']) {
- resolve(true);
- } else {
- resolve(false);
- }
- }
- });
- } else {
- resolve(false);
- }
- }
- });
- } else {
- // 请求权限
- uni.authorize({
- scope: 'scope.writePhotosAlbum',
- success: () => {
- resolve(true);
- },
- fail: () => {
- uni.showModal({
- title: '提示',
- content: '需要相册权限才能保存图片,是否前往设置?',
- success: (modalRes) => {
- if (modalRes.confirm) {
- uni.openSetting({
- success: (settingRes) => {
- if (settingRes.authSetting['scope.writePhotosAlbum']) {
- resolve(true);
- } else {
- resolve(false);
- }
- }
- });
- } else {
- resolve(false);
- }
- }
- });
- }
- });
- }
- },
- fail: () => {
- resolve(false);
- }
- });
- });
- }
- }
|