<!-- ---------------------------------------------------------------------
// uQRCode二维码生成插件 v3.5.1
// 
// uQRCode是一款基于Javascript环境开发的二维码生成插件，适用所有Javascript运行环境的前端应用和Node.js。
// 
// Copyright (c) Sansnn uQRCode All rights reserved.
// 
// Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// 
// github地址：
//   https://github.com/Sansnn/uQRCode
// 
// npm地址：
//   https://www.npmjs.com/package/@uqrcode/uni-app
// 
// uni-app插件市场地址：
//   https://ext.dcloud.net.cn/plugin?id=1287
// 
// 复制使用请保留本段注释，感谢支持开源！
// 
--------------------------------------------------------------------- -->
<template>
  <view class="uqrcode" :class="{ 'uqrcode-hide': hide }" :style="{ width: `${templateOptions.width}px`, height: `${templateOptions.height}px` }">
    <!-- 画布 -->
    <!-- #ifndef MP-WEIXIN || APP-NVUE -->
    <canvas
      class="uqrcode-canvas"
      :id="canvasId"
      :canvas-id="canvasId"
      :style="{
        width: `${templateOptions.canvasWidth}px`,
        height: `${templateOptions.canvasHeight}px`,
        transform: `scale(${size / templateOptions.canvasWidth}, ${size / templateOptions.canvasHeight})`
      }"
      @click="onClick"
      v-if="templateOptions.canvasDisplay"
    ></canvas>
    <!-- #endif -->

    <!-- 微信小程序非2d模式不支持transform所以使用canvas2d -->
    <!-- #ifdef MP-WEIXIN -->
    <canvas
      class="uqrcode-canvas"
      type="2d"
      :id="canvasId"
      :canvas-id="canvasId"
      :style="{
        width: `${templateOptions.canvasWidth}px`,
        height: `${templateOptions.canvasHeight}px`
      }"
      @click="onClick"
      v-if="templateOptions.canvasDisplay"
    ></canvas>
    <!-- #endif -->

    <!-- nvue用gcanvas -->
    <!-- #ifdef APP-NVUE -->
    <gcanvas
      class="uqrcode-canvas"
      ref="gcanvas"
      :style="{
        width: `${templateOptions.canvasWidth}px`,
        height: `${templateOptions.canvasHeight}px`
      }"
      @click="onClick"
      v-if="templateOptions.canvasDisplay"
    ></gcanvas>
    <!-- #endif -->

    <!-- H5保存提示 -->
    <!-- #ifdef H5 -->
    <view class="uqrcode-h5-save" v-if="isH5Save">
      <image class="uqrcode-h5-save-image" :src="tempFilePath"></image>
      <text class="uqrcode-h5-save-text">若保存失败，请长按二维码进行保存</text>
      <view class="uqrcode-h5-save-close" @click="isH5Save = false">
        <view class="uqrcode-h5-save-close-before"></view>
        <view class="uqrcode-h5-save-close-after"></view>
      </view>
    </view>
    <!-- #endif -->

    <!-- 加载效果，可在此替换 -->
    <view class="uqrcode-makeing" v-if="makeing">
      <image
        class="uqrcode-makeing-image"
        :style="{ width: `${size / 4}px`, height: `${size / 4}px` }"
        src="data:image/gif;base64, "
      ></image>
    </view>

    <!-- 错误处理，可在此替换 -->
    <view class="uqrcode-error" v-if="inError"><text class="uqrcode-error-message">Error, see console.</text></view>
  </view>
</template>

<script>
// #ifdef VUE3
import { toRaw } from 'vue';
// #endif
	
/* 引入uQRCode核心js */
import UQRCode from '../../js_sdk/uqrcode';

/* 引入nvue所需模块 */
// #ifdef APP-NVUE
import { enable, WeexBridge } from '../../js_sdk/gcanvas';
const modal = weex.requireModule('modal');
// #endif

export default {
  name: 'uqrcode',
  props: {
    /**
     * canvas组件id
     */
    canvasId: {
      type: String,
      required: true // canvasId在微信小程序初始值不能为空，created中赋值也不行，必须给一个值，否则挂载组件后无法绘制
    },
    /**
     * 二维码内容
     */
    value: {
      type: [String, Number]
    },
    /**
     * 二维码大小
     */
    size: {
      type: [String, Number],
      default: 200
    },
    /**
     * 选项
     */
    options: {
      type: Object,
      default: () => {
        return {};
      }
    },
    /**
     * 导出的文件类型
     */
    fileType: {
      type: String,
      default: 'png'
    },
    /**
     * 是否初始化组件后就开始生成
     */
    start: {
      type: Boolean,
      default: true
    },
    /**
     * 是否数据发生改变自动重绘
     */
    auto: {
      type: Boolean,
      default: false
    },
    /**
     * 隐藏组件
     */
    hide: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      canvas: undefined,
      canvasContext: undefined,
      makeDelegate: undefined,
      drawDelegate: undefined,
      toTempFilePathDelegate: undefined,
      makeing: false,
      drawing: false,
      inError: false,
      isH5Save: false,
      tempFilePath: '',
      templateOptions: {
        width: 0, // 组件宽度
        height: 0,
        canvasWidth: 0, // canvas宽度
        canvasHeight: 0,
        canvasDisplay: false
      },
      uqrcodeOptions: {
        data: ''
      },
      makeingPattern: [
        [
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true]
        ],
        [
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, false, false, false],
          [true, true, true, true, true, true, false, true, true, true],
          [true, true, true, true, true, true, false, true, true, true],
          [true, true, true, true, true, true, false, true, true, true]
        ],
        [
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, true, true, true, true, false, false, false],
          [true, true, true, true, true, true, true, false, false, false],
          [true, true, true, true, true, true, true, false, false, false],
          [true, true, true, false, false, false, false, true, true, true],
          [true, true, true, false, false, false, false, true, true, true]
        ],
        [
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, false, false, false, false, false, false, false],
          [true, true, true, false, false, false, false, false, false, false],
          [true, true, true, false, false, false, false, false, false, false],
          [true, true, true, false, false, false, false, false, false, false],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true],
          [true, true, true, true, true, true, true, true, true, true]
        ]
      ]
    };
  },
  watch: {
    value: {
      handler() {
        if (this.auto) {
          this.remake();
        }
      }
    },
    size: {
      handler() {
        if (this.auto) {
          this.remake();
        }
      }
    },
    options: {
      handler() {
        if (this.auto) {
          this.remake();
        }
      },
      deep: true
    },
    makeing: {
      handler(val) {
        if (!val) {
          if (typeof this.toTempFilePathDelegate === 'function') {
            this.toTempFilePathDelegate();
          }
        }
      }
    }
  },
  mounted() {
    this.templateOptions.canvasWidth = this.size;
    this.templateOptions.canvasHeight = this.size;
    if (this.start) {
      this.make();
    }
  },
  methods: {
    /**
     * 获取模板选项
     */
    getTemplateOptions() {
      return UQRCode.deepReplace(this.templateOptions, {
        width: this.size,
        height: this.size
      });
    },
    /**
     * 获取插件选项
     */
    getUqrcodeOptions() {
      return UQRCode.deepReplace(this.options, {
        data: String(this.value),
        size: Number(this.size)
      });
    },
    /**
     * 重置画布
     */
    resetCanvas(callback) {
      this.templateOptions.canvasDisplay = false;
      this.$nextTick(() => {
        this.templateOptions.canvasDisplay = true;
        this.$nextTick(() => {
          callback && callback();
        });
      });
    },
    /**
     * 绘制二维码
     */
    async draw(callback = success => {}) {
      if (this.drawing) {
        this.drawDelegate = () => {
          this.draw(callback);
        };
      }
      this.drawing = true;

      this.inError = false;
      if (!this.canvasId) {
        console.error('[uQRCode]: canvasId must be set!');
        this.inError = true;
        callback(false);
        return;
      }
      if (!this.value) {
        console.error('[uQRCode]: value must be set!');
        this.inError = true;
        callback(false);
        return;
      }

      /* 组件数据 */
      this.templateOptions = this.getTemplateOptions();
      /* uQRCode选项 */
      this.uqrcodeOptions = this.getUqrcodeOptions();
      /* 纠错等级兼容字母写法 */
      if (typeof this.uqrcodeOptions.errorCorrectLevel === 'string') {
        this.uqrcodeOptions.errorCorrectLevel = UQRCode.errorCorrectLevel[this.uqrcodeOptions.errorCorrectLevel];
      }
      /* nvue不支持动态修改gcanvas尺寸，除nvue外，默认使用useDynamicSize */
      // #ifndef APP-NVUE
      if (typeof this.options.useDynamicSize === 'undefined') {
        this.uqrcodeOptions.useDynamicSize = true;
      }
      // #endif
      // #ifdef APP-NVUE
      this.uqrcodeOptions.useDynamicSize = false;
      // #endif

      /* 获取uQRCode实例 */
      const qr = new UQRCode();
      /* 设置uQRCode选项 */
      qr.setOptions(this.uqrcodeOptions);
      /* 调用制作二维码方法 */
      qr.make();

      /* 获取canvas上下文 */
      // #ifndef MP-WEIXIN || APP-NVUE
      /* uniapp获取canvas上下文方式 */
      const canvasContext = (this.canvasContext = uni.createCanvasContext(this.canvasId, this));
      /* 使用dynamicSize，可以解决小块间出现白线问题，再通过scale缩放至size，使其达到所设尺寸 */
      this.templateOptions.canvasWidth = qr.dynamicSize;
      this.templateOptions.canvasHeight = qr.dynamicSize;
      /* uniapp获取图像方式 */
      UQRCode.loadImage = function(src) {
        return new Promise((resolve, reject) => {
          uni.getImageInfo({
            src,
            success: res => {
              resolve(res.path);
            },
            fail: err => {
              reject(err);
            }
          });
        });
      };
      // #endif
      // #ifdef MP-WEIXIN
      /* 微信小程序获取canvas上下文方式 */
      const canvas = (this.canvas = await new Promise(resolve => {
        uni
          .createSelectorQuery()
          .in(this) // 在组件内使用需要
          .select(`#${this.canvasId}`)
          .fields({
            node: true,
            size: true
          })
          .exec(res => {
            resolve(res[0].node);
          });
      }));
      const canvasContext = (this.canvasContext = canvas.getContext('2d'));
      /* 2d的组件设置宽高与实际canvas绘制宽高不是一个，打个比方，组件size=200，canvas.width设置为100，那么绘制出来就是100=200，组件size=400，canvas.width设置为800，绘制大小还是800=400，所以无需理会下方返回的dynamicSize是多少，按dpr重新赋值给canvas即可 */
      this.templateOptions.canvasWidth = qr.size;
      this.templateOptions.canvasHeight = qr.size;
      /* 使用dynamicSize+scale，可以解决小块间出现白线问题，dpr可以解决模糊问题 */
      const dpr = uni.getSystemInfoSync().pixelRatio;
      canvas.width = qr.dynamicSize * dpr;
      canvas.height = qr.dynamicSize * dpr;
      canvasContext.scale(dpr, dpr);
      /* 微信小程序获取图像方式 */
      UQRCode.loadImage = function(src) {
        /* 小程序下获取网络图片信息需先配置download域名白名单才能生效 */
        return new Promise((resolve, reject) => {
          const img = canvas.createImage();
          img.src = src;
          img.onload = () => {
            resolve(img);
          };
          img.onerror = err => {
            reject(err);
          };
        });
      };
      // #endif
      // #ifdef APP-NVUE
      /* NVue获取canvas上下文方式 */
      const gcanvas = this.$refs['gcanvas'];
      const canvas = enable(gcanvas, {
        bridge: WeexBridge
      });
      const canvasContext = (this.canvasContext = canvas.getContext('2d'));
      /* NVue获取图像方式 */
      UQRCode.loadImage = function(src) {
        return new Promise((resolve, reject) => {
          /* getImageInfo在nvue的bug：获取同一个路径的图片信息，同一时间第一次获取成功，后续失败，猜测是写入本地时产生文件写入冲突，所以没有返回，特别是对于网络资源 --- js部分已实现队列绘制，已解决此问题 */
          uni.getImageInfo({
            src,
            success: res => {
              resolve(res.path);
            },
            fail: err => {
              reject(err);
            }
          });
        });
      };
      // #endif

      /* 设置uQRCode实例的canvas上下文 */
      qr.canvasContext = canvasContext;
      /* 延时等待页面重新绘制完毕 */
      setTimeout(() => {
        /* 调用绘制方法将二维码图案绘制到canvas上 */
        qr.drawCanvas()
          .then(() => {
            if (this.drawDelegate) {
              /* 高频重绘纠正 */
              this.drawDelegate();
              this.drawDelegate = undefined;
            } else {
              this.drawing = false;
              callback(true);
            }
          })
          .catch(() => {
            if (this.drawDelegate) {
              this.drawDelegate();
              this.drawDelegate = undefined;
            } else {
              this.inError = true;
              this.drawing = false;
              callback(false);
            }
          });
      }, 300);
    },
    /**
     * 生成二维码
     */
    make() {
      this.makeing = true;
      this.resetCanvas(() => {
        clearTimeout(this.makeDelegate);
        this.makeDelegate = setTimeout(() => {
          this.draw(res => {
            this.makeing = false;
            this.complete(res);
          });
        }, 300);
      });
    },
    /**
     * 重新生成
     */
    remake() {
      this.make();
    },
    /**
     * 生成完成
     */
    complete(success = true) {
      setTimeout(() => {
        this.$emit('complete', {
          success
        });
      }, 300);
    },
    /**
     * 导出临时路径
     */
    toTempFilePath(callback = {}) {
      if (typeof callback.success != 'function') {
        callback.success = () => {};
      }
      if (typeof callback.fail != 'function') {
        callback.fail = () => {};
      }
      if (typeof callback.complete != 'function') {
        callback.complete = () => {};
      }

      if (this.makeing) {
        /* 如果还在生成状态，那当前操作将托管到委托，监听生成完成后再通过委托复调当前方法 */
        this.toTempFilePathDelegate = () => {
          this.toTempFilePath(callback);
        };
        return;
      } else {
        this.toTempFilePathDelegate = null;
      }

      // #ifndef MP-WEIXIN || APP-NVUE
      uni.canvasToTempFilePath(
        {
          canvasId: this.canvasId,
          fileType: this.fileType,
          width: this.templateOptions.canvasWidth,
          height: this.templateOptions.canvasHeight,
          success: res => {
            callback.success(res);
          },
          fail: err => {
            callback.fail(err);
          },
          complete: () => {
            callback.complete();
          }
        },
        this
      );
      // #endif

      // #ifdef MP-WEIXIN
      /* 需要将 data:image/png;base64, 这段去除 writeFile 才能正常打开文件，否则是损坏文件，无法打开*/
      const reg = new RegExp('^data:image/png;base64,', 'g');
	  // #ifdef VUE3
      const dataURL = toRaw(this.canvas).toDataURL().replace(reg, '');
	  // #endif
	  // #ifndef VUE3
	  const dataURL = this.canvas.toDataURL().replace(reg, '');
	  // #endif
      const fs = wx.getFileSystemManager();
      const tempFilePath = `${wx.env.USER_DATA_PATH}/${new Date().getTime()}${
        Math.random()
          .toString()
          .split('.')[1]
      }.png`;
      fs.writeFile({
        filePath: tempFilePath, // 要写入的文件路径 (本地路径)
        data: dataURL, // base64图片
        encoding: 'base64',
        success: res => {
          callback.success({
            tempFilePath
          });
        },
        fail: err => {
          callback.fail(err);
        },
        complete: () => {
          callback.complete();
        }
      });
      // #endif

      // #ifdef APP-NVUE
      /* 测试过程中，size需要乘以3才能保存完整，3又对应dpr，猜测是与像素比有关，故乘以3。第一次运行无法保存，后续正常，待排查。 */
      const dpr = uni.getSystemInfoSync().pixelRatio;
      this.canvasContext.toTempFilePath(0, 0, this.size * dpr, this.size * dpr, this.size * dpr, this.size * dpr, '', 1, res => {
        callback.success(res);
        callback.complete(res);
      });
      // #endif
    },
    /**
     * 保存
     */
    save(callback = {}) {
      if (typeof callback.success != 'function') {
        callback.success = () => {};
      }
      if (typeof callback.fail != 'function') {
        callback.fail = () => {};
      }
      if (typeof callback.complete != 'function') {
        callback.complete = () => {};
      }

      this.toTempFilePath({
        success: res => {
          // #ifndef H5
          uni.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: res1 => {
              callback.success(res1);
            },
            fail: err1 => {
              callback.fail(err1);
            },
            complete: () => {
              callback.complete();
            }
          });
          // #endif

          // #ifdef H5
          /* 可以在电脑浏览器下载，移动端iOS不行，安卓微信浏览器不行，安卓外部浏览器可以 */
          this.isH5Save = true;
          this.tempFilePath = res.tempFilePath;
          const aEle = document.createElement('a');
          aEle.download = 'uQRCode'; // 设置下载的文件名，默认是'下载'
          aEle.href = res.tempFilePath;
          document.body.appendChild(aEle);
          aEle.click();
          aEle.remove(); // 下载之后把创建的元素删除
          callback.success({
            errMsg: 'ok'
          });
          callback.complete();
          // #endif
        },
        fail: err => {
          callback.fail(err);
          callback.complete();
        }
      });
    },
    /**
     * 注册click事件
     */
    onClick(e) {
      this.$emit('click', e);
    }
  }
};
</script>

<style scoped>
.uqrcode {
  position: relative;
}

.uqrcode-hide {
  position: fixed;
  left: 7500rpx;
}

.uqrcode-canvas {
  transform-origin: top left;
}

.uqrcode-makeing {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 10;
  /* #ifndef APP-NVUE */
  display: flex;
  /* #endif */
  justify-content: center;
  align-items: center;
}

.uqrcode-makeing-image {
  /* #ifndef APP-NVUE */
  display: block;
  max-width: 120px;
  max-height: 120px;
  /* #endif */
}

.uqrcode-error {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  /* #ifndef APP-NVUE */
  display: flex;
  /* #endif */
  justify-content: center;
  align-items: center;
}

.uqrcode-error-message {
  font-size: 12px;
  color: #939291;
}

/* #ifdef H5 */
.uqrcode-h5-save {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  background-color: rgba(0, 0, 0, 0.68);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.uqrcode-h5-save-image {
  width: 512rpx;
  height: 512rpx;
  padding: 32rpx;
}

.uqrcode-h5-save-text {
  margin-top: 20rpx;
  font-size: 32rpx;
  font-weight: 700;
  color: #ffffff;
}

.uqrcode-h5-save-close {
  position: relative;
  margin-top: 72rpx;
  width: 40rpx;
  height: 40rpx;
  border: 2rpx solid #ffffff;
  border-radius: 40rpx;
  padding: 10rpx;
}

.uqrcode-h5-save-close-before {
  position: absolute;
  top: 50%;
  transform: translateY(-50%) rotate(45deg);
  width: 40rpx;
  height: 4rpx;
  background: #ffffff;
}

.uqrcode-h5-save-close-after {
  position: absolute;
  top: 50%;
  transform: translateY(-50%) rotate(-45deg);
  width: 40rpx;
  height: 4rpx;
  background: #ffffff;
}

/* #endif */
</style>
