Przeglądaj źródła

feat:添加海报保存图片功能和分享海报功能

wangsisi 3 tygodni temu
rodzic
commit
a1e189648d
3 zmienionych plików z 698 dodań i 104 usunięć
  1. 46 44
      components/posterPopup/posterPopup.vue
  2. 114 60
      pages/tabBar/tree/tree.vue
  3. 538 0
      utils/canvasUtils.js

+ 46 - 44
components/posterPopup/posterPopup.vue

@@ -44,16 +44,13 @@
 		</view>
 		<view class="footer">
 			<view class="btn" @click="handleDownload">保存图片</view>
-			<!-- <view class="share btn" @click="handleShare">去分享</view> -->
 			<button class="share btn" open-type="share">去分享</button>
 		</view>
-		<view class="close">
+		<view class="close">
 			<up-icon name="close-circle-fill" size="30" @click="showPopup = false"
-				color="rgba(255, 255, 255, 0.7)"></up-icon>
+				color="rgba(255, 255, 255, 0.7)"></up-icon>
 		</view>
 	</up-popup>
-	<!-- 保存弹窗 -->
-	<!-- <save-photo-popup ref="savePhotoDom" height="480"></save-photo-popup> -->
 </template>
 
 <script setup>
@@ -61,10 +58,9 @@
 		ref,
 		watch
 	} from "vue";
-	// import savePhotoPopup from "@/components/common/savePhotoPopup.vue";
-	import TREE from '@/api/tree.js'
-	import {
-		onShareAppMessage
+	import TREE from '@/api/tree.js'
+	import {
+		onShareAppMessage
 	} from '@dcloudio/uni-app'
 	import config from "@/api/config.js"
 	const resize = "?x-oss-process=image/resize,w_1000";
@@ -104,12 +100,12 @@
 		}
 	);
 
-	onShareAppMessage((res) => {
-		return {
-			title: '我分享了我的果树,快来查看吧~',
-			path: `/pages/tabBar/tree/subPages/friendTree?sampleId=${props.sampleId}`, // 分享的小程序页面路径  
-			imageUrl: `http://birdseye-api.feiniaotech.sysuimars.cn/mini/z_farm_buy/genImage/${props.farmBuyId}?key=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9&x1=25&y1=220&fontSize1=40&x2=55&y2=250&fontSize2=16&timestamp=${Date.now()}`,
-		}
+	onShareAppMessage((res) => {
+		return {
+			title: '我分享了我的果树,快来查看吧~',
+			path: `/pages/tabBar/tree/subPages/friendTree?sampleId=${props.sampleId}`, // 分享的小程序页面路径  
+			imageUrl: `http://birdseye-api.feiniaotech.sysuimars.cn/mini/z_farm_buy/genImage/${props.farmBuyId}?key=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9&x1=25&y1=220&fontSize1=40&x2=55&y2=250&fontSize2=16&timestamp=${Date.now()}`,
+		}
 	})
 
 	const showPopup = ref(false);
@@ -127,29 +123,36 @@
 	}
 
 	const treeObj = ref({});
-	const savePhotoDom = ref(null);
 	const contentDom = ref(null);
 
+	const emit = defineEmits(['download-poster']);
+	
 	const handleDownload = () => {
-		// savePhotoDom.value.handleDownload(contentDom.value);
+		// 将数据传递给父组件处理canvas
+		emit('download-poster', {
+			treeObj: treeObj.value,
+			treeName: props.treeName,
+			userInfo
+		});
 	};
 
-	const getPosterData = (farmBuyId) => {
-		TREE.getPoster({farmBuyId}).then(({data,code,msg}) =>{
-			if (code === 0) {
-				showPopup.value = true;
-				treeObj.value = {
-					...data,
-					watermarkArr: data.watermarkMsg && data.watermarkMsg.split(","),
-					...formatDate(data.createDate)
-				};
-			}else{
-				uni.showToast({
-				  title: msg,
-				  icon: 'none',
-				  duration: 2000
-				})
-			}
+	const getPosterData = (farmBuyId) => {
+		TREE.getPoster({farmBuyId}).then(({data,code,msg}) =>{
+			if (code === 0) {
+				showPopup.value = true;
+				treeObj.value = {
+					...data,
+					// posterUrl: 'https://birdseye-img.sysuimars.com/birdseye-look-mini/temp/1751086680814.jpg',
+					watermarkArr: data.watermarkMsg && data.watermarkMsg.split(","),
+					...formatDate(data.createDate)
+				};
+			}else{
+				uni.showToast({
+				  title: msg,
+				  icon: 'none',
+				  duration: 2000
+				})
+			}
 		})
 	};
 
@@ -175,7 +178,7 @@
 
 			.time {
 				line-height: 100rpx;
-				margin-top: -42rpx;
+				margin-top: -42rpx;
 				font-family: "SweiSpringCJKtc";
 
 				.date {
@@ -188,7 +191,7 @@
 			}
 
 			.qr-code {
-				text-align: right;
+				text-align: right;
 				font-family: "SweiSpringCJKtc";
 
 				.image {
@@ -214,7 +217,6 @@
 			.image {
 				width: 100%;
 				height: 100%;
-				object-fit: cover;
 				border-radius: 10rpx;
 			}
 
@@ -273,23 +275,23 @@
 			justify-content: space-between;
 
 			.text {
-				font-size: 22rpx;
-				font-family: "SweiSpringCJKtc";
+				font-size: 22rpx;
+				font-family: "SweiSpringCJKtc";
 				line-height: 32rpx;
 
 			}
 
 			.nickname {
 				font-size: 24rpx;
-				text-align: center;
+				text-align: center;
 
 				.logo {
 					width: 68rpx;
 					height: 72rpx;
 					margin-bottom: 8rpx;
-				}
-				.text{
-					font-family: "SweiSpringCJKtc";
+				}
+				.text{
+					font-family: "SweiSpringCJKtc";
 				}
 			}
 		}
@@ -309,7 +311,7 @@
 			border: 1px solid #fff;
 			background: #fff;
 			text-align: center;
-			color: #000;
+			color: #000;
 			line-height: inherit;
 		}
 
@@ -321,7 +323,7 @@
 	}
 
 	.close {
-		margin-top: 36rpx;
+		margin-top: 36rpx;
 		display: flex;
 		justify-content: center;
 	}

+ 114 - 60
pages/tabBar/tree/tree.vue

@@ -78,7 +78,13 @@
 		<!-- 编辑树名称 -->
 		<editNamePopup ref="editNameRef" @editEnd="getBySampleId"></editNamePopup>
 		<!-- 海报弹窗 -->
-		<posterPopup :showPoster="showPoster" :farmBuyId="farmBuyId" :treeName="treeName" :sampleId="sampleId"></posterPopup>
+		<posterPopup 
+			:showPoster="showPoster" 
+			:farmBuyId="farmBuyId" 
+			:treeName="treeName" 
+			:sampleId="sampleId"
+			@download-poster="handleDownloadPoster"
+		></posterPopup>
 		<!-- 果树成功弹窗 -->
 		<guardSuccessPopup :show="showGuardSuccess" :treeData="treeData"></guardSuccessPopup>
 		<!-- 果树相册弹窗 -->
@@ -88,6 +94,11 @@
 		<!-- 祝福弹窗 -->
 		<blessingsPopup :show="showBlessingsPopup" :showSuccess="showSuccess" :farmBuyId="farmBuyId"
 			:clockinType="clockinType" @clockinCallback="getBySampleId"></blessingsPopup>
+		
+		<!-- canvas生成图片 - 隐藏的canvas元素 -->
+		<canvas id="posterCanvas" canvas-id="posterCanvas" style="width: 690rpx;height: 1140rpx;position: fixed;left: 750rpx;top: 2000rpx;z-index: 9999;"></canvas>
+		
+
 	</view>
 </template>
 
@@ -103,16 +114,22 @@
 	import memberLevel from "./components/memberLevel.vue"
 	import treeAlbumPopup from "./components/treeAlbumPopup.vue"
 	import blessingsPopup from "./components/blessingsPopup.vue"
+	import checkinPopup from "@/components/checkinPopup/checkinPopup.vue"
+	import editNamePopup from "@/components/editNamePopup/editNamePopup.vue"
+	import guardSuccessPopup from "@/components/guardSuccessPopup/guardSuccessPopup.vue"
+	import levelSuccessPopup from "@/components/levelSuccessPopup/levelSuccessPopup.vue"
 	import danmakuManager from "@/components/danmakuManager/danmakuManager.vue"
+	import posterPopup from "@/components/posterPopup/posterPopup.vue"
 	import TREE from '@/api/tree.js'
 	import USER from '@/api/user.js'
+	import { CanvasUtils } from '@/utils/canvasUtils.js'
 	import {
 		ref
 	} from 'vue';
 
 	const name = ref('荔枝')
 	const showPicker = ref(false)
-	const columns = ref([]);
+	const columns = ref([]);
 	const speciesId = ref('')
 	const handleShow = () => {
 		showPicker.value = true
@@ -121,7 +138,7 @@
 		showPicker.value = false
 	}
 	const handleConfirm = (e) => {
-		name.value = e.value[0].name
+		name.value = e.value[0].name
 		speciesId.value = e.value[0].speciesId
 		handleCancel()
 	}
@@ -178,22 +195,22 @@
 			showName: treeData.value.buyList[0].showName,
 			createDate: formatDate(treeData.value.buyList[0].createDate),
 		})
-	}
-	
+	}
+	
 	const userInfo = ref({})
 
-	onLoad(() => {
+	onLoad(() => {
 		featchIsLookedGuide()
-		featchCategoryList()
+		featchCategoryList()
 	})
 	
-	onShow(()=>{
-		userInfo.value = uni.getStorageSync('userInfo')
-		const isUpdateName = uni.getStorageSync('isUpdateName')
-		if(isUpdateName === 'true'){
-			featchLightTree()
-			uni.setStorageSync('isUpdateName','false')
-		}
+	onShow(()=>{
+		userInfo.value = uni.getStorageSync('userInfo')
+		const isUpdateName = uni.getStorageSync('isUpdateName')
+		if(isUpdateName === 'true'){
+			featchLightTree()
+			uni.setStorageSync('isUpdateName','false')
+		}
 		featchSessionStore()
 		// 页面显示时启动弹幕
 		startDanmaku()
@@ -214,8 +231,8 @@
 		TREE.categoryList().then(({
 			data
 		}) => {
-			columns.value[0] = data || []
-			speciesId.value = data[0].speciesId
+			columns.value[0] = data || []
+			speciesId.value = data[0].speciesId
 			featchLightTree()
 		})
 	}
@@ -244,39 +261,39 @@
 		USER.getSessionStore({
 			key: 'successTree',
 			miniUserId: userInfo.value.id
-		}).then((({data}) => {
-			if(data?.text === 'orderSuccess'){
-				clockinType.value = 4
-				showSuccess.value = !showSuccess.value
-				treeSetSessionStore()
-			}else{
+		}).then((({data}) => {
+			if(data?.text === 'orderSuccess'){
+				clockinType.value = 4
+				showSuccess.value = !showSuccess.value
+				treeSetSessionStore()
+			}else{
 				if (data?.val == 0) {
-					showGuardSuccess.value = !showGuardSuccess.value
+					showGuardSuccess.value = !showGuardSuccess.value
 					treeSetSessionStore()
 				}
-			}
+			}
 		}))
-	}
-	
-	const treeSetSessionStore = () =>{
-		USER.setSessionStore({
-			key: 'successTree',
-			miniUserId: userInfo.value.id,
-			val:1,
-			text:'1'
-		})
 	}
 	
-	const closedCheckPopup = () =>{
+	const treeSetSessionStore = () =>{
+		USER.setSessionStore({
+			key: 'successTree',
+			miniUserId: userInfo.value.id,
+			val:1,
+			text:'1'
+		})
+	}
+	
+	const closedCheckPopup = () =>{
 		showPoster.value = !showPoster.value
 	}
 	
 	const sampleId = ref('')
 	//通过品类查询当前树
-	const featchLightTree = () =>{
-		TREE.getLightTreeList().then((res =>{
-			sampleId.value = res.data[0].sampleId
-			getBySampleId()
+	const featchLightTree = () =>{
+		TREE.getLightTreeList().then((res =>{
+			sampleId.value = res.data[0].sampleId
+			getBySampleId()
 		}))
 	}
 
@@ -386,6 +403,43 @@
 			danmakuRef.value.stopDanmakuAnimation()
 		}
 	}
+	// 创建canvas工具类实例
+	const canvasUtils = new CanvasUtils('posterCanvas')
+	
+	// 处理海报下载
+	const handleDownloadPoster = async (data) => {
+		
+		// 检查数据完整性
+		if (!data || !data.treeObj) {
+			uni.showToast({
+				title: '数据不完整,无法生成海报',
+				icon: 'none'
+			});
+			return;
+		}
+		
+		try {
+			uni.showLoading({
+				title: '正在生成海报...',
+				mask: true
+			});
+			
+			const success = await canvasUtils.generateAndSave(data);
+			
+			uni.hideLoading();
+
+		} catch (error) {
+			uni.hideLoading();
+			console.error('海报处理出错:', error);
+			uni.showToast({
+				title: '海报生成失败: ' + error.message,
+				icon: 'none'
+			});
+		}
+	}
+
+
+
 	const handleItem = (index) => {
 		if (index === 0) {
 			clockinType.value = 1
@@ -415,9 +469,9 @@
 				showSuccess.value = !showSuccess.value
 			}
 		} else {
-			uni.navigateTo({
-				url: `/pages/tabBar/home/subPages/gardenItem`
-			});
+			uni.navigateTo({
+				url: `/pages/tabBar/home/subPages/gardenItem`
+			});
 		}
 	}
 
@@ -661,25 +715,25 @@
 				.icon {
 					width: 96rpx;
 					height: 96rpx;
-				}
-				.share-btn {
-					background: transparent;
-					display: inline-flex;
-					.icon {
-						width: 90rpx;
-						height: 90rpx;
-						margin-top: 6rpx;
-					}
-				
-					&::after {
-						border: none;
-					}
-				}
-				
-				.small{
-					width: 90rpx;
-					height: 90rpx;
-					margin-top: 6rpx;
+				}
+				.share-btn {
+					background: transparent;
+					display: inline-flex;
+					.icon {
+						width: 90rpx;
+						height: 90rpx;
+						margin-top: 6rpx;
+					}
+				
+					&::after {
+						border: none;
+					}
+				}
+				
+				.small{
+					width: 90rpx;
+					height: 90rpx;
+					margin-top: 6rpx;
 				}
 
 				.name {

+ 538 - 0
utils/canvasUtils.js

@@ -0,0 +1,538 @@
+// 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 {
+				// 基础参数
+				const w = uni.upx2px(690)
+				const top = uni.upx2px(0)
+				const r = uni.upx2px(24)
+				
+				// 计算内容布局参数
+				const watermarkArr = data.treeObj.watermarkArr || [];
+				const lineHeight = uni.upx2px(40); // 水印文字行间距
+				const startY = uni.upx2px(870); // 水印文字起始Y坐标
+				const lastWatermarkY = startY + ((watermarkArr.length - 1) * lineHeight);
+				
+				const logoHeight = uni.upx2px(80);
+				const logoSpacing = uni.upx2px(26);
+				const logoY = lastWatermarkY - logoHeight - logoSpacing;
+				const brandTextY = logoY + logoHeight + logoSpacing;
+				
+				// 使用固定高度,确保有足够空间显示所有内容
+				// 计算canvas高度(根据内容自适应)
+				const bottomMargin = uni.upx2px(20); // 底部边距
+				const h = brandTextY + uni.upx2px(30) + 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)}px "SweiSpringCJKtc", Arial, sans-serif`  // 设置字体样式:粗体 + 自定义字体
+				ctx.fillText(data.treeObj.year, uni.upx2px(26), uni.upx2px(34))
+
+				// 绘制月份
+				ctx.font = `bold ${uni.upx2px(172)}px "SweiSpringCJKtc", Arial, sans-serif`  // 设置字体样式:粗体 + 自定义字体,172rpx大小
+				ctx.fillText(data.treeObj.monthNumber, uni.upx2px(26), uni.upx2px(76))
+				
+				// 绘制斜线
+				ctx.setLineWidth(uni.upx2px(4))  // 设置线条宽度
+				ctx.beginPath()
+				ctx.moveTo(uni.upx2px(160), uni.upx2px(180))  // 斜线起点(右上角)
+				ctx.lineTo(uni.upx2px(140), uni.upx2px(220))  // 斜线终点(左下角)
+				ctx.stroke()
+				
+				// 绘制日
+				ctx.font = `bold ${uni.upx2px(48)}px "SweiSpringCJKtc", Arial, sans-serif`  // 设置字体样式:粗体 + 自定义字体,172rpx大小
+				ctx.fillText(data.treeObj.day, uni.upx2px(162), uni.upx2px(180))
+
+				// 绘制二维码图片
+				if (downloadedImages.qrCodeUrl) {
+					try {
+						ctx.drawImage(downloadedImages.qrCodeUrl, uni.upx2px(w - -190), uni.upx2px(34), uni.upx2px(130), uni.upx2px(142));
+					} catch (error) {
+					}
+				}
+				
+				// 添加品种
+				ctx.setFontSize(uni.upx2px(24))
+				ctx.fillText(data.treeObj.pz, uni.upx2px(w - -218), uni.upx2px(190))
+
+				// 绘制树龄
+				ctx.setFontSize(uni.upx2px(24))
+				ctx.fillText(`${data.treeObj.age}年 ${data.treeObj.age > 9 ? "老树" : "树龄"}`, uni.upx2px(w - -104), uni.upx2px(190))
+				
+				// 绘制气候适宜
+				ctx.setFontSize(uni.upx2px(24))
+				ctx.fillText(data.treeObj.phenology, uni.upx2px(w - -32), uni.upx2px(230))
+
+				// 绘制采摘方式
+				ctx.setFontSize(uni.upx2px(24))
+				ctx.fillText(`气候适宜-${data.treeObj.howTxt || "果园采摘"}`, uni.upx2px(w - -114), uni.upx2px(230))
+
+				// 绘制树图片(带圆角)
+				const imgX = uni.upx2px(26);
+				const imgY = uni.upx2px(280);
+				const imgW = uni.upx2px(640);
+				const imgH = uni.upx2px(480);
+				const radius = uni.upx2px(10);
+				
+				// 创建圆角路径
+				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(w - -104);
+				const textY = uni.upx2px(316);
+				const textWidth = uni.upx2px(180); // 背景宽度
+				const textHeight = uni.upx2px(18);  // 背景高度
+				const padding = uni.upx2px(16);     // 内边距
+				const textRadius = uni.upx2px(30);  // 圆角半径
+				
+				// 绘制黑色半透明背景(圆角)
+				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));
+				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)}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), uni.upx2px(486), uni.upx2px(276), uni.upx2px(274));
+					} catch (error) {
+						console.error('树牌背景图片绘制失败:', error);
+					}
+				}
+
+				// 绘制树牌文字
+				ctx.setFillStyle('#ffffff');
+				ctx.font = `bold ${uni.upx2px(36)}px "jiangxizhuokai", Arial, sans-serif` 
+				ctx.fillText(`【${data.treeName}】`, uni.upx2px(178), uni.upx2px(565))
+
+				ctx.font = `bold ${uni.upx2px(18)}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), uni.upx2px(610))
+				
+				// 绘制横线(在水印文字上面)
+				ctx.setStrokeStyle('#000000');  // 设置线条颜色为黑色
+				ctx.setLineWidth(uni.upx2px(2));
+				
+				// 计算第一行水印文字的长度
+				const firstWatermarkText = data.treeObj.watermarkArr[0] || '';
+				ctx.font = `bold ${uni.upx2px(24)}px "SweiSpringCJKtc", Arial, sans-serif`;
+				const textMetrics = ctx.measureText(firstWatermarkText);
+				const watermarkTextWidth = textMetrics.width;
+				
+				ctx.beginPath();
+				ctx.moveTo(uni.upx2px(36), uni.upx2px(820));  // 横线起点,左边距离36rpx
+				ctx.lineTo(uni.upx2px(36) + watermarkTextWidth, uni.upx2px(820));  // 横线终点,根据文字长度
+				ctx.stroke();
+
+				// 绘制水印文字(根据数组长度动态生成)
+				ctx.setFillStyle('#000000');
+				ctx.font = `bold ${uni.upx2px(24)}px "SweiSpringCJKtc", Arial, sans-serif` 
+				ctx.setTextAlign('left');  // 设置左对齐,确保第一个字在指定位置
+				
+				watermarkArr.forEach((text, index) => {
+					const y = startY + (index * lineHeight);
+					ctx.fillText(text, uni.upx2px(36), y);
+				});
+
+				// 绘制logo(与最后一条水印文字对齐)
+				if (downloadedImages.logoUrl) {
+					try {
+						ctx.drawImage(downloadedImages.logoUrl, uni.upx2px(w - -230), logoY, uni.upx2px(76), logoHeight);
+					} catch (error) {
+						console.error('logo图片绘制失败:', error);
+					}
+				}
+
+				// 绘制飞鸟有味(与logo保持间距)
+				ctx.setFillStyle('#000000');
+				ctx.font = `bold ${uni.upx2px(22)}px "SweiSpringCJKtc", Arial, sans-serif` 
+				ctx.fillText(`飞鸟有味`, uni.upx2px(w - -225), 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: 690,
+					height: 1140, // 固定高度1140rpx
+					destWidth: 690,
+					destHeight: 1140, // 固定高度1140rpx
+					fileType: 'png',
+					success: (res) => {
+						uni.hideLoading();
+						
+						uni.showLoading({
+							title: '正在保存...',
+							mask: true
+						});
+						
+						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);
+				}
+			});
+		});
+	}
+}