uni-app微信小程序将海报与分享二维码相结合

606 阅读5分钟

今天要做的是将海报与微信分享二维码贴在一起生成完整的海报,并且要有切换图片的效果。一开始想着直接海报和分享二维码通过样式搞在一起,但是因为有缩小的特效,会导致切换图片时海报与二维码分离。所以我就换了个思路。

将图片和二维码加入到canvas,然后canvas转换成图片,这样整张的图片就不会有什么问题了。

e382260daadff749f566ba20c20682d.jpg

f906d6e2d411ab8428f3452b65d55f2.jpg

<template>
  <div class="s-container">
    <div class="s-title">
      <text>和好友一起推广,一起赚奖励</text>
    </div>
    <div class="s-main" 
    :style="{ 'padding-left': (screenWidth - basicNum) / 2 + 'px',
     'padding-right': (screenWidth - basicNum) / 2 + 'px' }">
      <div
        class="s-item"
        :class="{ active: currentIndex === index }"
        v-for="(item, index) in shareInfo"
        :key="index"
        :style="{
          right: item.right + '%',
          transform:
            (currentIndex === index ? 'scale(1)' : 'scale(.9)') +
            ' translateX(' +
            deltaX +
            'rpx)',
          transition: deltaX === 0 ? 'all .3s' : 'none',
          width: shareWidth + 'px', height: shareHeight + 'px',
        }"
        @click="setCurrentIndex(index)"
        @touchstart="touchstart"
        @touchmove="touchmove"
        @touchend="touchend"
      >
        <image
          :src="item.gestaltPosterImg ? item.gestaltPosterImg : item.posterImg"
        ></image>
        <canvas
          :style="{ width: shareWidth + 'px', height: shareHeight + 'px', }"
          :id="'shareCanvas' + index"
          :canvas-id="'shareCanvas' + index"
        ></canvas>
        <div class="s-current-info" v-show="currentIndex === index">
          <text>{{ currentIndex + 1 }}/{{ shareInfo.length }}</text>
        </div>
      </div>
    </div>
    <div class="s-footer">
      <div :class="{ 's-generate-bad': this.shareInfo.length === 0 }" class="s-generate" @tap="generatePosterImg">
        <image src="../../static/images/icon/picture.png" />
        <text>生成海报</text>
      </div>
    </div>

    <view class="code-popup" v-if="wxCodeShow" @tap="closeSharePopup()">
      <image class="code-main" 
      :class="{ 'fade-in': showExecShare, 'fade-out': !showExecShare }"
      :src="shareInfo[currentIndex].gestaltPosterImg" />

      <view
        @tap.stop
        class="share-main"
        :class="{ 'share-open': showExecShare, 'share-close': !showExecShare }"
      >
        <view class="share-close-btn" @tap="closeSharePopup()"></view>
        <view class="share-type">
          <button class="share-wechat" open-type="share">
            <image src="../../static/images/icon/share1.png"></image>
            <span>发送给朋友</span>
          </button>
          <button class="share-download" @tap="saveShareImg">
            <image src="../../static/images/icon/share3.png"></image>
            <span>保存至相册</span>
          </button>
        </view>
      </view>
    </view>
  </div>
</template>
<script>
var config = require("../../../utils/config.js");
var http = require("../../../utils/http.js");
export default {
  onLoad() {
    uni.setNavigationBarTitle({
      title: "分享中心",
    });

    const systemInfo = uni.getSystemInfoSync();
    this.screenWidth = systemInfo.screenWidth;
  },
  async onShow() {
    uni.showLoading();
    this.shareInfo = await this.initShareInfo();
    if (this.shareInfo.length) {
      this.shareWxCode = await this.createShareQrCodeImg();
      this.shareCodeImg = await this.getShareQrCodeImg();

      const rect = await this.getShareCodeBoxSize();
      this.initRate(rect);

      await this.initShareCanvas();
      this.initShareCanvasToImgs();
    } 
    uni.hideLoading();

    console.log(this.shareInfo);
  },
  onShareAppMessage() {
    this.closeSharePopup();
    return {
      title: "有蜜会员MALL",
      path: "/pages/index/index?scene" + wx.getStorageSync("distCardNo"),
      imageUrl: this.shareInfo[this.currentIndex].gestaltPosterImg
    }
  },
  data() {
    return {
      currentIndex: 0,
      screenWidth: 0,
      shareInfo: [],
      shareCodeImg: "",
      shareWxCode: "",
      startX: 0, // 触摸开始时的 X 坐标
      currentX: 0, // 触摸移动时的 X 坐标
      deltaX: 0, // 滑动的距离
      shareWidth: 0, //图片的宽度 * resultRate
      shareHeight: 0, //图片的高度 * resultRate
      basicRate: 0.72, //基准比例值
      resultRate: 0, //结果比例值
      basicNum: 0, //basicRate * screenWidth
      wxCodeShow: false,
      showExecShare: false,
    };
  },
  methods: {
    initShareInfo() {
      return new Promise((resolve) => {
        http.request({
          url: "/poster/list",
          method: "GET",
          data: {},
          callBack: (res) => {
            res.forEach((v) => {
              v.right = 0;
              v.gestaltPosterImg = "";
            });
            resolve(res);
          },
          errCallBack: (err) => {
            resolve([]);
          },
        });
      });
    },
    setCurrentIndex(index) {
      if (this.currentIndex !== index) {
        this.currentIndex = index;
      }
    },
    touchstart(event) {
      // 获取触摸开始时的 X 坐标
      this.startX = event.touches[0].clientX;
    },
    touchmove(event) {
      // 获取触摸移动时的 X 坐标
      this.currentX = event.touches[0].clientX;

      // 计算滑动的距离
      this.deltaX = this.currentX - this.startX;
    },
    touchend() {
      if (Math.abs(this.deltaX) > 70) {
        // 向右滑动
        if (this.deltaX > 0 && this.currentIndex > 0) {
          setTimeout(() => {
            this.currentIndex--;
          });
        }
        // 向左滑动
        if (this.deltaX < 0 && this.currentIndex < this.shareInfo.length - 1) {
          setTimeout(() => {
            this.currentIndex++;
          });
        }
      }
      this.deltaX = 0;
    },
    getShareQrCodeImg() {
      return new Promise((resolve) => {
        var cardno = wx.getStorageSync("distCardNo");
        wx.downloadFile({
          header: {
            Authorization: wx.getStorageSync("token"),
          },
          url:
            config.domain +
            "/p/distribution/qrCode/invitation?page=pages/index/index&scene=" +
            cardno,
          success: (res) => {
            resolve(res.tempFilePath);
          },
          fail: (err) => {
            resolve("");
          },
        });
      });
    },
    async createShareQrCodeImg() {
      return new Promise((resolve) => {
        const cardNo = wx.getStorageSync("distCardNo");
        const content = JSON.stringify({
          scene: cardNo,
        });
        const params = {
          url: "/qrcodeTicket/miniQrCode",
          method: "GET",
          responseType: "arraybuffer",
          data: {
            type: 2,
            content: content,
          },
          callBack: async (res) => {
            resolve("data:image/jpg;base64," + wx.arrayBufferToBase64(res));
          },
          errCallBack: (err) => {
            resolve("");
          },
        };
        http.request(params);
      });
    },
    initRate(boxSize) {
      this.basicNum = this.screenWidth * this.basicRate;

      this.resultRate = (this.basicNum / boxSize.width).toFixed(2);

      this.shareWidth = boxSize.width * this.resultRate;
      this.shareHeight = boxSize.height * this.resultRate;
    },
    getShareCodeBoxSize() {
      return new Promise(resolve => {
        const img = this.shareInfo[0].posterImg;
        uni.getImageInfo({
          src: img,
          success: (res) => {
            resolve({
              width: res.width,
              height: res.height
            })
          },
        });
      })
    },
    async initShareCanvas() {
      for (let i = 0; i < this.shareInfo.length; i++) {
        const id = "shareCanvas" + i;
        await this.createShareCanvas(id, i);
      }
    },
    async createShareCanvas(canvasId, idx) {
      const ctx = uni.createCanvasContext(canvasId, this);
      ctx.setFillStyle("#ffffff");
      this.roundRect(ctx, 0, 0, this.shareWidth, this.shareHeight, 10);
      ctx.fill();
      const img = await this.getImageInfo(this.shareInfo[idx].posterImg);
      this.roundRectImg(ctx, 0, 0, this.shareWidth, this.shareHeight, 10, img);

      const size = 0.26 * this.shareWidth;
      const widthRate = this.shareInfo[idx].leftAbsolute;
      const heightRate = this.shareInfo[idx].topAbsolute;
      this.roundRectImg(
        ctx,
        widthRate * this.resultRate,
        heightRate * this.resultRate,
        size,
        size,
        size / 2,
        this.shareCodeImg
      );
      ctx.draw();
    },
    roundRect(ctx, x, y, width, height, radius) {
      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();
    },
    roundRectImg(ctx, x, y, width, height, radius, img) {
      ctx.save();
      this.roundRect(ctx, x, y, width, height, radius);
      ctx.clip();
      ctx.drawImage(img, x, y, width, height);
      ctx.restore();
    },
    getImageInfo(path) {
      return new Promise((resolve, reject) => {
        uni.getImageInfo({
          src: path,
          success: (res) => {
            resolve(res.path);
          },
          fail: (err) => {
            reject(err);
          },
        });
      });
    },
    initShareCanvasToImgs() {
      const query = uni.createSelectorQuery();
      for (let i = 0; i < this.shareInfo.length; i++) {
        const id = "shareCanvas" + i;
        query
          .select("#" + id)
          .node()
          .exec((res) => {
            const canvasNode = res[0].node;
            // 将canvas内容转换为临时文件路径
            uni.canvasToTempFilePath(
              {
                canvasId: id,
                success: (res) => {
                  this.shareInfo[i].gestaltPosterImg = res.tempFilePath;
                },
                fail: (err) => {
                  console.log("转换失败", err);
                },
              },
              canvasNode
            );
          });
      }
    },
    async saveShareImg() {
      uni.saveImageToPhotosAlbum({
        filePath: this.shareInfo[this.currentIndex].gestaltPosterImg,
        success: () => {
          uni.hideLoading();
          uni.showToast({
            title: "保存成功",
          });
        },
        fail: (err) => {
          uni.hideLoading();
          uni.showToast({
            title: "保存失败",
            icon: "none",
          });
        },
      });
    },
    generatePosterImg() {
      if (this.shareInfo.length === 0) {
        uni.showToast({
          title: "暂时还没有海报哦",
          icon: "none",
        });
        return;
      }
      this.openSharePopup();
    },
    closeSharePopup() {
      this.showExecShare = false;

      setTimeout(() => {
        this.setData({
          wxCodeShow: false,
        });
      }, 300);
    },
    openSharePopup() {
      this.wxCodeShow = true;

      setTimeout(() => {
        this.showExecShare = true;
      }, 300);
    },
  },
  watch: {
    currentIndex(newValue, oldValue) {
      if (oldValue > newValue) {
        this.shareInfo.forEach((v) => (v.right -= 100));
      }
      if (oldValue < newValue) {
        this.shareInfo.forEach((v) => (v.right += 100));
      }
    },
  },
};
</script>
<style>
page {
  background: linear-gradient(to bottom, #fff4fa, #f4f4f4);
  overflow: hidden;
}
</style>

<style scoped>
.s-container {
  width: 100%;
  overflow: hidden;
}

.s-title {
  text-align: center;
  margin: 32rpx;
  background: linear-gradient(to right, #f02850, #ff280f);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.s-main {
  display: flex;
  width: 100%;
  box-sizing: border-box;
  overflow: hidden;
  position: relative;
}

.s-item {
  flex-shrink: 0;
  position: relative;
}

.s-item > canvas {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 9999px;
}

.s-item > image {
  width: 100%;
  height: 100%;
}

.s-item::after {
  content: "";
  display: block;
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  border-radius: 20rpx;
  background-color: rgba(0, 0, 0, 0.4);
}

.s-item:last-child {
  margin: 0;
}

.active::after {
  content: none;
}

.s-footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  background-color: #fff;
  height: 160rpx;
  display: flex;
  justify-content: center;
  align-items: center;
}

.s-footer > .s-generate {
  width: 92%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  padding: 20rpx 0;
  border-radius: 62rpx;
  background: linear-gradient(to right, #f02850, #ff280f);
}

.s-footer > .s-generate-bad {
  background: none;
  background-color: #666;
}

.s-footer > .s-generate > image {
  width: 42rpx;
  height: 42rpx;
  margin-right: 12rpx;
}

.s-current-info {
  position: absolute;
  right: 20rpx;
  bottom: 10rpx;
  font-size: 22rpx;
  color: #fff;
  padding: 6rpx 22rpx;
  border-radius: 32rpx;
  background-color: rgba(0, 0, 0, 0.6);
}

.code-popup {
  position: fixed;
  top: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.7);
  width: 100%;
  height: 100%;
  z-index: 9999;
}

.code-main {
  transition: all .4s ;
  margin: auto;
  left: 0;
  right: 0;
  top: 200rpx;
  position: fixed;
  width: 560rpx;
  height: 848rpx;
  box-sizing: border-box;
}

.fade-in {
  opacity: 1;
}

.fade-out {
  opacity: 0;
}

.share-main {
  width: 100%;
  height: 260rpx;
  padding-top: 28rpx;
  background-color: #fff;
  position: absolute;
  bottom: -300rpx;
  border-top-left-radius: 32rpx;
  border-top-right-radius: 32rpx;
  transition: all .3s;
}

.share-open {
  bottom: 0;
}

.share-close {
  bottom: -300rpx;
}

.share-close-btn {
  position: absolute;
  right: 32rpx;
	width: 24px;
	height: 24px;
}

.share-close-btn::before,
.share-close-btn::after {
	content: "";
	position: absolute;
  top: 50%;
	left: 50%;
	width: 24px;
	height: 2px;
	background-color: #EEE;
	transform: translate(-50%, -50%);
}

.share-close-btn::before {
	transform: translate(-50%, -50%) rotate(45deg);
}

.share-close-btn::after {
	transform: translate(-50%, -50%) rotate(-45deg);
}

.share-type {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-around;
}

.share-type > button {
  padding: 0;
  margin: 0;
  border: none;
  outline: none;
  border-radius: none;
  background-color: transparent;
  font-size: 26rpx;
  color: #ccc;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.share-type > button::after {
  content: none !important;
}

.share-type image {
  width: 62rpx;
  height: 62rpx;
  border-radius: 50%;
}
</style>