哪有前端像我这么体面,不就是小程序合成图片嘛

84 阅读4分钟

1、背景

uniapp(vue3)开发小程序,点击海报图,将海报图与小程序码合成一张新图,并长按保存本地。

2、代码实现

  1. 布局
<template>
  <view class="poster">
    <view class="poster-tip">
      点击任意海报会生成是专属小程序码,长按可保存至本地
    </view>
    <!-- 绘制组合海报与小程序的图 -->
    <view class="poster-swiper">
      <up-swiper v-if="posterList && posterList.length" @click="onSwiper" height="426" :list="posterList"
        keyName="imageUrl" indicator indicatorMode="dot" indicatorActiveColor="#4396fe"
        indicatorInactiveColor="#D6D6D6" previousMargin="30" nextMargin="30" circular :autoplay="false" radius="5"
        bgColor="#ffffff"></up-swiper>
      <view v-else-if="loading" class="poster-null">
        <view class="poster-null-img">
          <image class="poster-null-icon" src="../../static/ic_poster_null.png" mode="widthFix"></image>
        </view>
        <view class="poster-null-label">
          暂未上传图片,如有需要请联系平台方
        </view>
      </view>
    </view>
    <canvas canvas-id="myCanvas" style="width: 320px; height: 426px; opacity: 0;margin-top:9999px"></canvas>
    <!-- 海报预览 -->
    <up-popup :show="showPoster" mode="center" @close="closePoster" @open="open" @touchMouve.prevent.stop>
      <!-- 已生成海报地址 -->
      <view class="poster-url" v-if="createPostUrl">
        <image :src="createPostUrl" class="poster-url-img" @longpress="onSave"></image>
        <!-- <canvas canvas-id="myCanvas" style="width: 320px; height: 426px;"></canvas> -->
      </view>
      <!--<view class="poster-url" v-else-if="postUrl">
      <image :src="postUrl" class="poster-url-img"></image>
      <image :src="codeUrl" class="poster-url-code">
    </image>
    </view>-->
    </up-popup>
  </template>
  1. 初始化
<script setup>
  import {
  onLoad
  } from "@dcloudio/uni-app";
  import {
  ref,
  toRaw
  } from 'vue';

  const userId = ref(10086)
  const posterList = ref([]) // 海报列表
  const copyPosterList = ref([]) // 原始海报列表
  const postUrl = ref("") // 点击的海报地址
  const createPostUrl = ref("") // 生成的海报地址
  const showPoster = ref(false) // 展示海报
  const provinceId = ref("000000"); // 选中省份ID
  const codeUrl = ref("/f574daa7f65d4b859330db671d0e3b8b.png")

  const closePoster = () => {
  showPoster.value = false
  }

  const getData = () => {
  posterList.value = [];
  // // todo调试代码
  posterList.value = [
  'https://cdn.uviewui.com/uview/swiper/swiper1.png',
  'https://cdn.uviewui.com/uview/swiper/swiper2.png',
  'https://cdn.uviewui.com/uview/swiper/swiper3.png',
  ];
  copyPosterList.value = [
  'https://cdn.uviewui.com/uview/swiper/swiper1.png',
  'https://cdn.uviewui.com/uview/swiper/swiper2.png',
  'https://cdn.uviewui.com/uview/swiper/swiper3.png',
  ];
  }

  onLoad(() => {
  console.log("初始化1")
  getData()
  })
</script>
  1. 点击图片
<script setup>
  //点击图片
  const onSwiper = (index) => {
    console.log("点击图片2", index);
    postUrl.value = posterList.value[index]
    // 存在已生成的海报
    if (posterList.value.length !== copyPosterList.value.length) {
      let cachePostUrlMap = uni.getStorageSync("postUrlMap")
      let key = `${provinceId.value}-${postUrl.value}`;
      //点击的是海报 存在已缓存生成的海报
      if (cachePostUrlMap && cachePostUrlMap[key]) {
        // 获取当前海报是否存在已生成的海报地址
        createPostUrl.value = cachePostUrlMap[key];
        // 生成海报放在第一位
        posterList.value.splice(0, 1, createPostUrl.value);
        // 更新首位图
        cachePostUrlMap[provinceId.value] = createPostUrl.value; // 每个省份仅缓存一张生成的最新生成的海报图片
        uni.setStorageSync("postUrlMap", cachePostUrlMap);
        showPoster.value = true;
      } else if (index == 0) {
        // 点击的是已生成的带有二维码的海报 默认将生成的代码海报插入swiper的首位
        createPostUrl.value = postUrl.value;
        showPoster.value = true;
      } else {
        console.log("生成海报3postUrl", postUrl.value)
        combineImages()
      }
    } else {
      console.log("生成海报3postUrl", postUrl.value)
      combineImages();
    }
  }
</script>
  1. 生成带有小程序码的海报图
// 合成海报
  const combineImages = async () => {
    uni.showLoading({
      title: '海报生成中'
    })
    const ctx = uni.createCanvasContext('myCanvas', this);
    // 加载需要结合的图片
    const imageInfo = await uni.getImageInfo({
      src: postUrl.value
    });
    // 绘制需要结合的图片到canvas上
    ctx.drawImage(imageInfo.path, 0, 0, 320, 426);
    console.log("combineImages=>imageInfo", imageInfo)

    // 加载小程序码图片
    const qrCodeInfo = await uni.getImageInfo({
      src: codeUrl.value
    });
    console.log("combineImages=>qrCodeInfo", codeUrl.value, qrCodeInfo)
    // 绘制小程序码到canvas上,位置为右下角,大小为100px * 100px
    const qrCodeWidth = 100;
    const qrCodeHeight = 100;
    const x = (320 - qrCodeWidth) / 2; // 计算小程序码的x坐标
    const y = 426 - qrCodeHeight - 10; // 计算小程序码的y坐标
    ctx.drawImage(qrCodeInfo.path, x, y, qrCodeWidth, qrCodeHeight);

    // 将canvas内容导出为图片
    ctx.draw(false, () => {
      uni.canvasToTempFilePath({
        canvasId: 'myCanvas',
        success: (res) => {
          console.log('合并后的图片路径4:', res.tempFilePath);
          uploadFile(res.tempFilePath);
        },
        fail: (err) => {
          console.error('绘制失败:', err);
        }
      });
    });
  };
  1. 上传生成的图片获取服务器地址
	// 使用微信方法上传
	const uploadFile = (res) => {
		uni.showLoading({
			title: '上传中',
			mask: true,
		});
		// 需要根据环境切换下协议
		let url = "http://xxx";
		uni.uploadFile({
			url,
			header: {
				"content-type": "multipart/form-data",
			},
			filePath: res,
			name: 'file',
			formData: {
				processData: false,
				contentType: false,
				cache: false
			},
			success: uploadFileRes => {
				uni.hideLoading();
				console.log(uploadFileRes, 'uploadFileRes5');
				let resData = JSON.parse(uploadFileRes.data);
				console.log('resData', resData);
				if (resData.success && resData.datas && resData.datas.longUrl) {
					createPostUrl.value = resData.datas.longUrl;
					showPoster.value = true;
					console.log("createPostUrl", createPostUrl.value);
					// 1.缓存当前图片 规则 key 为 用户Id-省份Id-原图地址 val 合成图地址
					// 定义 postUrlMap
					let postUrlMapKey = "postUrlMap"
					// 更新postUrlMapVal
					let cachePostUrlMap = uni.getStorageSync(postUrlMapKey)
					let postUrlMapVal = cachePostUrlMap || {
						"id": userId.value
					}
					let key = `${provinceId.value}-${postUrl.value}` // 每个省份仅缓存一张生成的图片
					postUrlMapVal[key] = createPostUrl.value;
					postUrlMapVal[provinceId.value] = createPostUrl.value; // 每个省份仅缓存一张生成的最新生成的海报图片
					uni.setStorageSync(postUrlMapKey, postUrlMapVal);
					console.log("已生成海报上传 缓存5", postUrlMapVal, createPostUrl.value, toRaw(copyPosterList.value))
					// 1.判断是否存在已生成的海报 如存在则替换 不存在则插入
					if (posterList.value.length == copyPosterList.value.length) {
						posterList.value.unshift(createPostUrl.value);
					} else {
						posterList.value.splice(0, 1, createPostUrl.value);
					}
					console.log("更新轮播组件7", toRaw(posterList.value))
				} else {
					uni.showToast({
						icon: 'none',
						title: '上传失败,请重试',
					});
				}
			},
			fail: e => {
				uni.showToast({
					icon: 'none',
					title: '上传失败,请重试',
				});
			},
		});
	}
  1. 长按保存到生成的海报到本地
	// 海报保存到本地
	const onSave = () => {
		uni.showLoading({
			title: "保存中..."
		})
		uni.downloadFile({
			url: createPostUrl.value,
			success: (res) => {
				if (res.statusCode === 200) {
					console.log('下载成功');
					console.log("res", res.tempFilePath)
					uni.saveImageToPhotosAlbum({
						filePath: res.tempFilePath,
						success: function(data) {
							uni.hideLoading()
							uni.showToast({
								title: '保存成功',
								icon: 'success',
								duration: 2000
							})
						},
						fail: function(err) {
							wx.hideLoading()
							if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny" || err.errMsg ===
								"saveImageToPhotosAlbum:fail:auth denied") {
								console.log("当初用户拒绝,再次发起授权")
								uni.showModal({
									title: '提示',
									content: '需要您授权保存相册',
									showCancel: false,
									success: () => {
										uni.openSetting({
											success(settingdata) {
												console.log("settingdata", settingdata)
												if (settingdata.authSetting['scope.writePhotosAlbum']) {
													uni.showModal({
														title: '提示',
														content: '获取权限成功,再次点击保持到相册即可保存',
														showCancel: false,
													})
												} else {
													wx.showModal({
														title: '提示',
														content: '获取权限失败,将无法保存到相册哦~',
														showCancel: false,
													})
												}
											},
											fail(failData) {
												console.log("failData", failData)
											},
											complete(finishData) {
												console.log("finishData", finishData)
											}
										})
									}
								})
							} else {
								console.log("err", err)
								uni.showToast({
									icon: "none",
									title: "保存失败,请重试!",
									duration: 2000
								})
							}
						},
						complete(res) {
							console.log(res);
							// wx.hideLoading()
						}
					});
				} else {
					uni.showToast({
						icon: "none",
						title: "保存失败,请重试!",
						duration: 2000
					})
				}
			},
			fail(err) {
				console.error('下载失败', err);
				uni.showToast({
					title: '下载失败',
					icon: 'none',
					duration: 2000
				});
			}
		});
	};

3.注意事项:如何在看不到canvas的情况下,进行正常绘制图片?

关于canvas隐藏,一开始使用了opacity:0,因为脱离层级,层级高,在真机上展示异常处理 解决方案: 给在外层加个css .poster { over-flow:hidden } 给canvas加个margin-top:9999px 进行隐藏

4.Q&A

如何提高合成图片的清晰度?