Переглянути джерело

修改体质调查问卷和睡眠调查问卷不能提交bug
修改体制报告数值为nan和null bug
修改体制报告列表数值为nan和null bug
体质调查问卷增加点击 以上没有 js特效

suixueyan 6 місяців тому
батько
коміт
12b1772489

+ 54 - 0
componets/painter/lib/calc.js

@@ -0,0 +1,54 @@
+/* eslint-disable */
+// 四则运算
+
+!(function () {
+  var calculate = function (s) {
+    s = s.trim();
+    const stack = new Array();
+    let preSign = '+';
+    let numStr = '';
+    const n = s.length;
+    for (let i = 0; i < n; ++i) {
+      if (s[i] === '.' || (!isNaN(Number(s[i])) && s[i] !== ' ')) {
+        numStr += s[i];
+      } else if (s[i] === '(') {
+        let isClose = 1;
+        let j = i;
+        while (isClose > 0) {
+          j += 1;
+          if (s[j] === '(') isClose += 1;
+          if (s[j] === ')') isClose -= 1;
+        }
+        numStr = `${calculate(s.slice(i + 1, j))}`;
+        i = j;
+      }
+      if ((isNaN(Number(s[i])) && s[i] !== '.') || i === n - 1) {
+        let num = parseFloat(numStr);
+        switch (preSign) {
+          case '+':
+            stack.push(num);
+            break;
+          case '-':
+            stack.push(-num);
+            break;
+          case '*':
+            stack.push(stack.pop() * num);
+            break;
+          case '/':
+            stack.push(stack.pop() / num);
+            break;
+          default:
+            break;
+        }
+        preSign = s[i];
+        numStr = '';
+      }
+    }
+    let ans = 0;
+    while (stack.length) {
+      ans += stack.pop();
+    }
+    return ans;
+  };
+  module.exports = calculate;
+})();

+ 363 - 0
componets/painter/lib/downloader.js

@@ -0,0 +1,363 @@
+/**
+ * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
+ * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
+ */
+const util = require('./util');
+const sha1 = require('./sha1');
+
+const SAVED_FILES_KEY = 'savedFiles';
+const KEY_TOTAL_SIZE = 'totalSize';
+const KEY_PATH = 'path';
+const KEY_TIME = 'time';
+const KEY_SIZE = 'size';
+
+// 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
+let MAX_SPACE_IN_B = 6 * 1024 * 1024;
+let savedFiles = {};
+
+export default class Dowloader {
+  constructor() {
+    // app 如果设置了最大存储空间,则使用 app 中的
+    if (getApp().PAINTER_MAX_LRU_SPACE) {
+      MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
+    }
+    wx.getStorage({
+      key: SAVED_FILES_KEY,
+      success: function (res) {
+        if (res.data) {
+          savedFiles = res.data;
+        }
+      },
+    });
+  }
+
+  /**
+   * 下载文件,会用 lru 方式来缓存文件到本地
+   * @param {String} url 文件的 url
+   */
+  download(url, lru) {
+    return new Promise((resolve, reject) => {
+      if (!(url && util.isValidUrl(url))) {
+        resolve(url);
+        return;
+      }
+      const fileName = getFileName(url);
+      if (!lru) {
+        // 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
+        wx.getFileInfo({
+          filePath: fileName,
+          success: () => {
+            resolve(url);
+          },
+          fail: () => {
+            if (util.isOnlineUrl(url)) {
+              downloadFile(url, lru).then((path) => {
+                resolve(path);
+              }, () => {
+                reject();
+              });
+            } else if (util.isDataUrl(url)) {
+              transformBase64File(url, lru).then(path => {
+                resolve(path);
+              }, () => {
+                reject();
+              });
+            }
+          },
+        })
+        return
+      }
+
+      const file = getFile(fileName);
+
+      if (file) {
+        if (file[KEY_PATH].indexOf('//usr/') !== -1) {
+          wx.getFileInfo({
+            filePath: file[KEY_PATH],
+            success() {
+              resolve(file[KEY_PATH]);
+            },
+            fail(error) {
+              console.error(`base64 file broken, ${JSON.stringify(error)}`);
+              transformBase64File(url, lru).then(path => {
+                resolve(path);
+              }, () => {
+                reject();
+              });
+            }
+          })
+        } else {
+          // 检查文件是否正常,不正常需要重新下载
+          wx.getSavedFileInfo({
+            filePath: file[KEY_PATH],
+            success: (res) => {
+              resolve(file[KEY_PATH]);
+            },
+            fail: (error) => {
+              console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
+              downloadFile(url, lru).then((path) => {
+                resolve(path);
+              }, () => {
+                reject();
+              });
+            },
+          });
+        }
+      } else {
+        if (util.isOnlineUrl(url)) {
+          downloadFile(url, lru).then((path) => {
+            resolve(path);
+          }, () => {
+            reject();
+          });
+        } else if (util.isDataUrl(url)) {
+          transformBase64File(url, lru).then(path => {
+            resolve(path);
+          }, () => {
+            reject();
+          });
+        }
+      }
+    });
+  }
+}
+
+function getFileName(url) {
+  if (util.isDataUrl(url)) { 
+    const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
+    const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
+    return fileName;
+  } else {
+    return url;
+  }
+}
+
+function transformBase64File(base64data, lru) {
+  return new Promise((resolve, reject) => {
+    const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
+    if (!format) {
+      console.error('base parse failed');
+      reject();
+      return;
+    }
+    const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
+    const path = `${wx.env.USER_DATA_PATH}/${fileName}`;
+    const buffer = wx.base64ToArrayBuffer(bodyData.replace(/[\r\n]/g, ""));
+    wx.getFileSystemManager().writeFile({
+      filePath: path,
+      data: buffer,
+      encoding: 'binary',
+      success() {
+        wx.getFileInfo({
+          filePath: path,
+          success: (tmpRes) => {
+            const newFileSize = tmpRes.size;
+            lru ? doLru(newFileSize).then(() => {
+              saveFile(fileName, newFileSize, path, true).then((filePath) => {
+                resolve(filePath);
+              });
+            }, () => {
+              resolve(path);
+            }) : resolve(path);
+          },
+          fail: (error) => {
+          // 文件大小信息获取失败,则此文件也不要进行存储
+            console.error(`getFileInfo ${path} failed, ${JSON.stringify(error)}`);
+            resolve(path);
+          },
+        });
+      },
+      fail(err) {
+        console.log(err)
+      }
+    })
+  });  
+}
+
+function downloadFile(url, lru) {
+  return new Promise((resolve, reject) => {
+    const downloader = url.startsWith('cloud://')?wx.cloud.downloadFile:wx.downloadFile
+    downloader({
+      url: url,
+      fileID: url,
+      success: function (res) {
+        if (res.statusCode !== 200) {
+          console.error(`downloadFile ${url} failed res.statusCode is not 200`);
+          reject();
+          return;
+        }
+        const {
+          tempFilePath
+        } = res;
+        wx.getFileInfo({
+          filePath: tempFilePath,
+          success: (tmpRes) => {
+            const newFileSize = tmpRes.size;
+            lru ? doLru(newFileSize).then(() => {
+              saveFile(url, newFileSize, tempFilePath).then((filePath) => {
+                resolve(filePath);
+              });
+            }, () => {
+              resolve(tempFilePath);
+            }) : resolve(tempFilePath);
+          },
+          fail: (error) => {
+            // 文件大小信息获取失败,则此文件也不要进行存储
+            console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
+            resolve(res.tempFilePath);
+          },
+        });
+      },
+      fail: function (error) {
+        console.error(`downloadFile failed, ${JSON.stringify(error)} `);
+        reject();
+      },
+    });
+  });
+}
+
+function saveFile(key, newFileSize, tempFilePath, isDataUrl = false) {
+  return new Promise((resolve, reject) => {
+    if (isDataUrl) {
+      const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
+      savedFiles[key] = {};
+      savedFiles[key][KEY_PATH] = tempFilePath;
+      savedFiles[key][KEY_TIME] = new Date().getTime();
+      savedFiles[key][KEY_SIZE] = newFileSize;
+      savedFiles['totalSize'] = newFileSize + totalSize;
+      wx.setStorage({
+        key: SAVED_FILES_KEY,
+        data: savedFiles,
+      });
+      resolve(tempFilePath);
+      return;
+    }
+    wx.saveFile({
+      tempFilePath: tempFilePath,
+      success: (fileRes) => {
+        const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
+        savedFiles[key] = {};
+        savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
+        savedFiles[key][KEY_TIME] = new Date().getTime();
+        savedFiles[key][KEY_SIZE] = newFileSize;
+        savedFiles['totalSize'] = newFileSize + totalSize;
+        wx.setStorage({
+          key: SAVED_FILES_KEY,
+          data: savedFiles,
+        });
+        resolve(fileRes.savedFilePath);
+      },
+      fail: (error) => {
+        console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
+        // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
+        resolve(tempFilePath);
+        // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
+        reset();
+      },
+    });
+  });
+}
+
+/**
+ * 清空所有下载相关内容
+ */
+function reset() {
+  wx.removeStorage({
+    key: SAVED_FILES_KEY,
+    success: () => {
+      wx.getSavedFileList({
+        success: (listRes) => {
+          removeFiles(listRes.fileList);
+        },
+        fail: (getError) => {
+          console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
+        },
+      });
+    },
+  });
+}
+
+function doLru(size) {
+  if (size > MAX_SPACE_IN_B) {
+    return Promise.reject()
+  }
+  return new Promise((resolve, reject) => {
+    let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
+
+    if (size + totalSize <= MAX_SPACE_IN_B) {
+      resolve();
+      return;
+    }
+    // 如果加上新文件后大小超过最大限制,则进行 lru
+    const pathsShouldDelete = [];
+    // 按照最后一次的访问时间,从小到大排序
+    const allFiles = JSON.parse(JSON.stringify(savedFiles));
+    delete allFiles[KEY_TOTAL_SIZE];
+    const sortedKeys = Object.keys(allFiles).sort((a, b) => {
+      return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
+    });
+
+    for (const sortedKey of sortedKeys) {
+      totalSize -= savedFiles[sortedKey].size;
+      pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
+      delete savedFiles[sortedKey];
+      if (totalSize + size < MAX_SPACE_IN_B) {
+        break;
+      }
+    }
+
+    savedFiles['totalSize'] = totalSize;
+
+    wx.setStorage({
+      key: SAVED_FILES_KEY,
+      data: savedFiles,
+      success: () => {
+        // 保证 storage 中不会存在不存在的文件数据
+        if (pathsShouldDelete.length > 0) {
+          removeFiles(pathsShouldDelete);
+        }
+        resolve();
+      },
+      fail: (error) => {
+        console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
+        reject();
+      },
+    });
+  });
+}
+
+function removeFiles(pathsShouldDelete) {
+  for (const pathDel of pathsShouldDelete) {
+    let delPath = pathDel;
+    if (typeof pathDel === 'object') {
+      delPath = pathDel.filePath;
+    }
+    if (delPath.indexOf('//usr/') !== -1) {
+      wx.getFileSystemManager().unlink({
+        filePath: delPath,
+        fail(error) {
+          console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
+        }
+      })
+    } else {
+      wx.removeSavedFile({
+        filePath: delPath,
+        fail: (error) => {
+          console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
+        },
+      });
+    }
+  }
+}
+
+function getFile(key) {
+  if (!savedFiles[key]) {
+    return;
+  }
+  savedFiles[key]['time'] = new Date().getTime();
+  wx.setStorage({
+    key: SAVED_FILES_KEY,
+    data: savedFiles,
+  });
+  return savedFiles[key];
+}

+ 102 - 0
componets/painter/lib/gradient.js

@@ -0,0 +1,102 @@
+/* eslint-disable */
+// 当ctx传入当前文件,const grd = ctx.createCircularGradient() 和 
+// const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理
+// 先分析,在外部创建grd,再传入使用就可以
+
+!(function () {
+
+  var api = {
+    isGradient: function(bg) {
+      if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) {
+        return true;
+      } 
+      return false;
+    },
+
+    doGradient: function(bg, width, height, ctx) {
+      if (bg.startsWith('linear')) {
+        linearEffect(width, height, bg, ctx);
+      } else if (bg.startsWith('radial')) {
+        radialEffect(width, height, bg, ctx);
+      }
+    },
+  }
+
+  function analizeGrad(string) {
+    const colorPercents = string.substring(0, string.length - 1).split("%,");
+    const colors = [];
+    const percents = [];
+    for (let colorPercent of colorPercents) {
+      colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(" ")).trim());
+      percents.push(colorPercent.substring(colorPercent.lastIndexOf(" "), colorPercent.length) / 100);
+    }
+    return {colors: colors, percents: percents};
+  }
+
+  function radialEffect(width, height, bg, ctx) {
+    const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]);
+    const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, width < height ? height / 2 : width / 2);
+    for (let i = 0; i < colorPer.colors.length; i++) {
+      grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
+    }
+    ctx.fillStyle = grd;
+    //ctx.fillRect(-(width / 2), -(height / 2), width, height);
+  }
+
+  function analizeLinear(bg, width, height) {
+    const direction = bg.match(/([-]?\d{1,3})deg/);
+    const dir = direction && direction[1] ? parseFloat(direction[1]) : 0;
+    let coordinate;
+    switch (dir) {
+      case 0: coordinate = [0, -height / 2, 0, height / 2]; break;
+      case 90: coordinate = [width / 2, 0, -width / 2, 0]; break;
+      case -90: coordinate = [-width / 2, 0, width / 2, 0]; break;
+      case 180: coordinate = [0, height / 2, 0, -height / 2]; break;
+      case -180: coordinate = [0, -height / 2, 0, height / 2]; break;
+      default:
+        let x1 = 0;
+        let y1 = 0;
+        let x2 = 0;
+        let y2 = 0;
+        if (direction[1] > 0 && direction[1] < 90) {
+          x1 = (width / 2) - ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
+          y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else if (direction[1] > -180 && direction[1] < -90) {
+          x1 = -(width / 2) + ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
+          y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else if (direction[1] > 90 && direction[1] < 180) {
+          x1 = (width / 2) + (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
+          y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        } else {
+          x1 = -(width / 2) - (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
+          y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
+          x2 = -x1;
+          y1 = -y2;
+        }
+        coordinate = [x1, y1, x2, y2];
+      break;
+    }
+    return coordinate;
+  }
+
+  function linearEffect(width, height, bg, ctx) {
+    const param = analizeLinear(bg, width, height);
+    const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]);
+    const content = bg.match(/linear-gradient\((.+)\)/)[1];
+    const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1));
+    for (let i = 0; i < colorPer.colors.length; i++) {
+      grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
+    }
+    ctx.fillStyle = grd
+    //ctx.fillRect(-(width / 2), -(height / 2), width, height);
+  }
+
+  module.exports = { api }
+
+})();

+ 903 - 0
componets/painter/lib/pen.js

@@ -0,0 +1,903 @@
+const QR = require('./qrcode.js');
+const GD = require('./gradient.js');
+require('./string-polyfill.js');
+
+export const penCache = {
+  // 用于存储带 id 的 view 的 rect 信息
+  viewRect: {},
+  textLines: {},
+};
+export const clearPenCache = id => {
+  if (id) {
+    penCache.viewRect[id] = null;
+    penCache.textLines[id] = null;
+  } else {
+    penCache.viewRect = {};
+    penCache.textLines = {};
+  }
+};
+export default class Painter {
+  constructor(ctx, data) {
+    this.ctx = ctx;
+    this.data = data;
+  }
+
+  paint(callback) {
+    this.style = {
+      width: this.data.width.toPx(),
+      height: this.data.height.toPx(),
+    };
+
+    this._background();
+    for (const view of this.data.views) {
+      this._drawAbsolute(view);
+    }
+    this.ctx.draw(false, () => {
+      callback && callback();
+    });
+  }
+
+  _background() {
+    this.ctx.save();
+    const { width, height } = this.style;
+    const bg = this.data.background;
+    this.ctx.translate(width / 2, height / 2);
+
+    this._doClip(this.data.borderRadius, width, height);
+    if (!bg) {
+      // 如果未设置背景,则默认使用透明色
+      this.ctx.fillStyle = 'transparent';
+      this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
+    } else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') {
+      // 背景填充颜色
+      this.ctx.fillStyle = bg;
+      this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
+    } else if (GD.api.isGradient(bg)) {
+      GD.api.doGradient(bg, width, height, this.ctx);
+      this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
+    } else {
+      // 背景填充图片
+      this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height);
+    }
+    this.ctx.restore();
+  }
+
+  _drawAbsolute(view) {
+    if (!(view && view.type)) {
+      // 过滤无效 view
+      return;
+    }
+    // 证明 css 为数组形式,需要合并
+    if (view.css && view.css.length) {
+      /* eslint-disable no-param-reassign */
+      view.css = Object.assign(...view.css);
+    }
+    switch (view.type) {
+      case 'image':
+        this._drawAbsImage(view);
+        break;
+      case 'text':
+        this._fillAbsText(view);
+        break;
+      case 'inlineText':
+        this._fillAbsInlineText(view);
+        break;
+      case 'rect':
+        this._drawAbsRect(view);
+        break;
+      case 'qrcode':
+        this._drawQRCode(view);
+        break;
+      default:
+        break;
+    }
+  }
+
+  _border({ borderRadius = 0, width, height, borderWidth = 0, borderStyle = 'solid' }) {
+    let r1 = 0,
+      r2 = 0,
+      r3 = 0,
+      r4 = 0;
+    const minSize = Math.min(width, height);
+    if (borderRadius) {
+      const border = borderRadius.split(/\s+/);
+      if (border.length === 4) {
+        r1 = Math.min(border[0].toPx(false, minSize), width / 2, height / 2);
+        r2 = Math.min(border[1].toPx(false, minSize), width / 2, height / 2);
+        r3 = Math.min(border[2].toPx(false, minSize), width / 2, height / 2);
+        r4 = Math.min(border[3].toPx(false, minSize), width / 2, height / 2);
+      } else {
+        r1 = r2 = r3 = r4 = Math.min(borderRadius && borderRadius.toPx(false, minSize), width / 2, height / 2);
+      }
+    }
+    const lineWidth = borderWidth && borderWidth.toPx(false, minSize);
+    this.ctx.lineWidth = lineWidth;
+    if (borderStyle === 'dashed') {
+      this.ctx.setLineDash([(lineWidth * 4) / 3, (lineWidth * 4) / 3]);
+      // this.ctx.lineDashOffset = 2 * lineWidth
+    } else if (borderStyle === 'dotted') {
+      this.ctx.setLineDash([lineWidth, lineWidth]);
+    }
+    const notSolid = borderStyle !== 'solid';
+    this.ctx.beginPath();
+
+    notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
+    r1 !== 0 && this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1 + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI); //左上角圆弧
+    this.ctx.lineTo(
+      r2 === 0 ? (notSolid ? width / 2 : width / 2 + lineWidth / 2) : width / 2 - r2,
+      -height / 2 - lineWidth / 2,
+    ); // 顶边线
+
+    notSolid && r2 === 0 && this.ctx.moveTo(width / 2 + lineWidth / 2, -height / 2 - lineWidth); // 右边虚线规避重叠规则
+    r2 !== 0 && this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2 + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI); // 右上角圆弧
+    this.ctx.lineTo(
+      width / 2 + lineWidth / 2,
+      r3 === 0 ? (notSolid ? height / 2 : height / 2 + lineWidth / 2) : height / 2 - r3,
+    ); // 右边线
+
+    notSolid && r3 === 0 && this.ctx.moveTo(width / 2 + lineWidth, height / 2 + lineWidth / 2); // 底边虚线规避重叠规则
+    r3 !== 0 && this.ctx.arc(width / 2 - r3, height / 2 - r3, r3 + lineWidth / 2, 0, 0.5 * Math.PI); // 右下角圆弧
+    this.ctx.lineTo(
+      r4 === 0 ? (notSolid ? -width / 2 : -width / 2 - lineWidth / 2) : -width / 2 + r4,
+      height / 2 + lineWidth / 2,
+    ); // 底边线
+
+    notSolid && r4 === 0 && this.ctx.moveTo(-width / 2 - lineWidth / 2, height / 2 + lineWidth); // 左边虚线规避重叠规则
+    r4 !== 0 && this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4 + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI); // 左下角圆弧
+    this.ctx.lineTo(
+      -width / 2 - lineWidth / 2,
+      r1 === 0 ? (notSolid ? -height / 2 : -height / 2 - lineWidth / 2) : -height / 2 + r1,
+    ); // 左边线
+    notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
+
+    if (!notSolid) {
+      this.ctx.closePath();
+    }
+  }
+
+  /**
+   * 根据 borderRadius 进行裁减
+   */
+  _doClip(borderRadius, width, height, borderStyle) {
+    if (borderRadius && width && height) {
+      // 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
+      // globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white,相对默认的 black 要好点
+      this.ctx.globalAlpha = 0;
+      this.ctx.fillStyle = 'white';
+      this._border({
+        borderRadius,
+        width,
+        height,
+        borderStyle,
+      });
+      this.ctx.fill();
+      // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性
+      if (!(getApp().systemInfo && getApp().systemInfo.version <= '6.6.6' && getApp().systemInfo.platform === 'ios')) {
+        this.ctx.clip();
+      }
+      this.ctx.globalAlpha = 1;
+    }
+  }
+
+  /**
+   * 画边框
+   */
+  _doBorder(view, width, height) {
+    if (!view.css) {
+      return;
+    }
+    const { borderRadius, borderWidth, borderColor, borderStyle } = view.css;
+    if (!borderWidth) {
+      return;
+    }
+    this.ctx.save();
+    this._preProcess(view, true);
+    this.ctx.strokeStyle = borderColor || 'black';
+    this._border({
+      borderRadius,
+      width,
+      height,
+      borderWidth,
+      borderStyle,
+    });
+    this.ctx.stroke();
+    this.ctx.restore();
+  }
+
+  _preProcess(view, notClip) {
+    let width = 0;
+    let height;
+    let extra;
+    const paddings = this._doPaddings(view);
+    switch (view.type) {
+      case 'inlineText': {
+        {
+          // 计算行数
+          let lines = 0;
+          // 文字总长度
+          let textLength = 0;
+          // 行高
+          let lineHeight = 0;
+          const textList = view.textList || [];
+          for (let i = 0; i < textList.length; i++) {
+            let subView = textList[i];
+            const fontWeight = subView.css.fontWeight || '400';
+            const textStyle = subView.css.textStyle || 'normal';
+            if (!subView.css.fontSize) {
+              subView.css.fontSize = '20rpx';
+            }
+            this.ctx.font = `${textStyle} ${fontWeight} ${subView.css.fontSize.toPx()}px "${subView.css.fontFamily || 'sans-serif'}"`;
+            textLength += this.ctx.measureText(subView.text).width;
+            let tempLineHeight = subView.css.lineHeight ? subView.css.lineHeight.toPx() : subView.css.fontSize.toPx();
+            lineHeight = Math.max(lineHeight, tempLineHeight);
+          }
+          width = view.css.width ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3] : textLength;;
+          const calLines = Math.ceil(textLength / width);
+
+          lines += calLines;
+          // lines = view.css.maxLines < lines ? view.css.maxLines : lines;
+          height = lineHeight * lines;
+          extra = {
+            lines: lines,
+            lineHeight: lineHeight,
+            // textArray: textArray,
+            // linesArray: linesArray,
+          };
+        }
+        break;
+      }
+      case 'text': {
+        const textArray = String(view.text).split('\n');
+        // 处理多个连续的'\n'
+        for (let i = 0; i < textArray.length; ++i) {
+          if (textArray[i] === '') {
+            textArray[i] = ' ';
+          }
+        }
+        const fontWeight = view.css.fontWeight || '400';
+        const textStyle = view.css.textStyle || 'normal';
+        if (!view.css.fontSize) {
+          view.css.fontSize = '20rpx';
+        }
+        this.ctx.font = `${textStyle} ${fontWeight} ${view.css.fontSize.toPx()}px "${
+          view.css.fontFamily || 'sans-serif'
+        }"`;
+        // 计算行数
+        let lines = 0;
+        const linesArray = [];
+        for (let i = 0; i < textArray.length; ++i) {
+          const textLength = this.ctx.measureText(textArray[i]).width;
+          const minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
+          let partWidth = view.css.width
+            ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3]
+            : textLength;
+          if (partWidth < minWidth) {
+            partWidth = minWidth;
+          }
+          const calLines = Math.ceil(textLength / partWidth);
+          // 取最长的作为 width
+          width = partWidth > width ? partWidth : width;
+          lines += calLines;
+          linesArray[i] = calLines;
+        }
+        lines = view.css.maxLines < lines ? view.css.maxLines : lines;
+        const lineHeight = view.css.lineHeight ? view.css.lineHeight.toPx() : view.css.fontSize.toPx();
+        height = lineHeight * lines;
+        extra = {
+          lines: lines,
+          lineHeight: lineHeight,
+          textArray: textArray,
+          linesArray: linesArray,
+        };
+        break;
+      }
+      case 'image': {
+        // image的长宽设置成auto的逻辑处理
+        const ratio = getApp().systemInfo.pixelRatio ? getApp().systemInfo.pixelRatio : 2;
+        // 有css却未设置width或height,则默认为auto
+        if (view.css) {
+          if (!view.css.width) {
+            view.css.width = 'auto';
+          }
+          if (!view.css.height) {
+            view.css.height = 'auto';
+          }
+        }
+        if (!view.css || (view.css.width === 'auto' && view.css.height === 'auto')) {
+          width = Math.round(view.sWidth / ratio);
+          height = Math.round(view.sHeight / ratio);
+        } else if (view.css.width === 'auto') {
+          height = view.css.height.toPx(false, this.style.height);
+          width = (view.sWidth / view.sHeight) * height;
+        } else if (view.css.height === 'auto') {
+          width = view.css.width.toPx(false, this.style.width);
+          height = (view.sHeight / view.sWidth) * width;
+        } else {
+          width = view.css.width.toPx(false, this.style.width);
+          height = view.css.height.toPx(false, this.style.height);
+        }
+        break;
+      }
+      default:
+        if (!(view.css.width && view.css.height)) {
+          console.error('You should set width and height');
+          return;
+        }
+        width = view.css.width.toPx(false, this.style.width);
+        height = view.css.height.toPx(false, this.style.height);
+        break;
+    }
+    let x;
+    if (view.css && view.css.right) {
+      if (typeof view.css.right === 'string') {
+        x = this.style.width - view.css.right.toPx(true, this.style.width);
+      } else {
+        // 可以用数组方式,把文字长度计算进去
+        // [right, 文字id, 乘数(默认 1)]
+        const rights = view.css.right;
+        x =
+          this.style.width -
+          rights[0].toPx(true, this.style.width) -
+          penCache.viewRect[rights[1]].width * (rights[2] || 1);
+      }
+    } else if (view.css && view.css.left) {
+      if (typeof view.css.left === 'string') {
+        x = view.css.left.toPx(true, this.style.width);
+      } else {
+        const lefts = view.css.left;
+        x = lefts[0].toPx(true, this.style.width) + penCache.viewRect[lefts[1]].width * (lefts[2] || 1);
+      }
+    } else {
+      x = 0;
+    }
+    //const y = view.css && view.css.bottom ? this.style.height - height - view.css.bottom.toPx(true) : (view.css && view.css.top ? view.css.top.toPx(true) : 0);
+    let y;
+    if (view.css && view.css.bottom) {
+      y = this.style.height - height - view.css.bottom.toPx(true, this.style.height);
+    } else {
+      if (view.css && view.css.top) {
+        if (typeof view.css.top === 'string') {
+          y = view.css.top.toPx(true, this.style.height);
+        } else {
+          const tops = view.css.top;
+          y = tops[0].toPx(true, this.style.height) + penCache.viewRect[tops[1]].height * (tops[2] || 1);
+        }
+      } else {
+        y = 0;
+      }
+    }
+
+    const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0;
+    // 当设置了 right 时,默认 align 用 right,反之用 left
+    const align = view.css && view.css.align ? view.css.align : view.css && view.css.right ? 'right' : 'left';
+    const verticalAlign = view.css && view.css.verticalAlign ? view.css.verticalAlign : 'top';
+    // 记录绘制时的画布
+    let xa = 0;
+    switch (align) {
+      case 'center':
+        xa = x;
+        break;
+      case 'right':
+        xa = x - width / 2;
+        break;
+      default:
+        xa = x + width / 2;
+        break;
+    }
+    let ya = 0;
+    switch (verticalAlign) {
+      case 'center':
+        ya = y;
+        break;
+      case 'bottom':
+        ya = y - height / 2;
+        break;
+      default:
+        ya = y + height / 2;
+        break;
+    }
+    this.ctx.translate(xa, ya);
+    // 记录该 view 的有效点击区域
+    // TODO ,旋转和裁剪的判断
+    // 记录在真实画布上的左侧
+    let left = x;
+    if (align === 'center') {
+      left = x - width / 2;
+    } else if (align === 'right') {
+      left = x - width;
+    }
+    var top = y;
+    if (verticalAlign === 'center') {
+      top = y - height / 2;
+    } else if (verticalAlign === 'bottom') {
+      top = y - height;
+    }
+    if (view.rect) {
+      view.rect.left = left;
+      view.rect.top = top;
+      view.rect.right = left + width;
+      view.rect.bottom = top + height;
+      view.rect.x = view.css && view.css.right ? x - width : x;
+      view.rect.y = y;
+    } else {
+      view.rect = {
+        left: left,
+        top: top,
+        right: left + width,
+        bottom: top + height,
+        x: view.css && view.css.right ? x - width : x,
+        y: y,
+      };
+    }
+
+    view.rect.left = view.rect.left - paddings[3];
+    view.rect.top = view.rect.top - paddings[0];
+    view.rect.right = view.rect.right + paddings[1];
+    view.rect.bottom = view.rect.bottom + paddings[2];
+    if (view.type === 'text') {
+      view.rect.minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
+    }
+
+    this.ctx.rotate(angle);
+    if (!notClip && view.css && view.css.borderRadius && view.type !== 'rect') {
+      this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
+    }
+    this._doShadow(view);
+    if (view.id) {
+      penCache.viewRect[view.id] = {
+        width,
+        height,
+        left: view.rect.left,
+        top: view.rect.top,
+        right: view.rect.right,
+        bottom: view.rect.bottom,
+      };
+    }
+    return {
+      width: width,
+      height: height,
+      x: x,
+      y: y,
+      extra: extra,
+    };
+  }
+
+  _doPaddings(view) {
+    const { padding } = view.css ? view.css : {};
+    let pd = [0, 0, 0, 0];
+    if (padding) {
+      const pdg = padding.split(/\s+/);
+      if (pdg.length === 1) {
+        const x = pdg[0].toPx();
+        pd = [x, x, x, x];
+      }
+      if (pdg.length === 2) {
+        const x = pdg[0].toPx();
+        const y = pdg[1].toPx();
+        pd = [x, y, x, y];
+      }
+      if (pdg.length === 3) {
+        const x = pdg[0].toPx();
+        const y = pdg[1].toPx();
+        const z = pdg[2].toPx();
+        pd = [x, y, z, y];
+      }
+      if (pdg.length === 4) {
+        const x = pdg[0].toPx();
+        const y = pdg[1].toPx();
+        const z = pdg[2].toPx();
+        const a = pdg[3].toPx();
+        pd = [x, y, z, a];
+      }
+    }
+    return pd;
+  }
+
+  // 画文字的背景图片
+  _doBackground(view) {
+    this.ctx.save();
+    const { width: rawWidth, height: rawHeight } = this._preProcess(view, true);
+
+    const { background } = view.css;
+    let pd = this._doPaddings(view);
+    const width = rawWidth + pd[1] + pd[3];
+    const height = rawHeight + pd[0] + pd[2];
+
+    this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
+    if (GD.api.isGradient(background)) {
+      GD.api.doGradient(background, width, height, this.ctx);
+    } else {
+      this.ctx.fillStyle = background;
+    }
+    this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
+
+    this.ctx.restore();
+  }
+
+  _drawQRCode(view) {
+    this.ctx.save();
+    const { width, height } = this._preProcess(view);
+    QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color);
+    this.ctx.restore();
+    this._doBorder(view, width, height);
+  }
+
+  _drawAbsImage(view) {
+    if (!view.url) {
+      return;
+    }
+    this.ctx.save();
+    const { width, height } = this._preProcess(view);
+    // 获得缩放到图片大小级别的裁减框
+    let rWidth = view.sWidth;
+    let rHeight = view.sHeight;
+    let startX = 0;
+    let startY = 0;
+    // 绘画区域比例
+    const cp = width / height;
+    // 原图比例
+    const op = view.sWidth / view.sHeight;
+    if (cp >= op) {
+      rHeight = rWidth / cp;
+      startY = Math.round((view.sHeight - rHeight) / 2);
+    } else {
+      rWidth = rHeight * cp;
+      startX = Math.round((view.sWidth - rWidth) / 2);
+    }
+    if (view.css && view.css.mode === 'scaleToFill') {
+      this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height);
+    } else {
+      this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height);
+      view.rect.startX = startX / view.sWidth;
+      view.rect.startY = startY / view.sHeight;
+      view.rect.endX = (startX + rWidth) / view.sWidth;
+      view.rect.endY = (startY + rHeight) / view.sHeight;
+    }
+    this.ctx.restore();
+    this._doBorder(view, width, height);
+  }
+  /**
+   * 
+   * @param {*} view 
+   * @description 一行内文字多样式的方法
+   * 
+   * 暂不支持配置 text-align,默认left
+   * 暂不支持配置 maxLines
+   */
+  _fillAbsInlineText(view) {
+    if (!view.textList) {
+      return;
+    }
+    if (view.css.background) {
+      // 生成背景
+      this._doBackground(view);
+    }
+    this.ctx.save();
+    const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius);
+    const { lines, lineHeight } = extra;
+    let staticX = -(width / 2);
+    let lineIndex = 0; // 第几行
+    let x = staticX; // 开始x位置
+    let leftWidth = width; // 当前行剩余多少宽度可以使用
+
+    let getStyle = css => {
+      const fontWeight = css.fontWeight || '400';
+      const textStyle = css.textStyle || 'normal';
+      if (!css.fontSize) {
+        css.fontSize = '20rpx';
+      }
+      return `${textStyle} ${fontWeight} ${css.fontSize.toPx()}px "${css.fontFamily || 'sans-serif'}"`;
+    }
+
+    // 遍历行内的文字数组
+    for (let j = 0; j < view.textList.length; j++) {
+      const subView = view.textList[j];
+
+      // 某个文字开始位置
+      let start = 0;
+      // 文字已使用的数量
+      let alreadyCount = 0;
+      // 文字总长度
+      let textLength = subView.text.length;
+      // 文字总宽度
+      let textWidth = this.ctx.measureText(subView.text).width;
+      // 每个文字的平均宽度
+      let preWidth = Math.ceil(textWidth / textLength);
+
+      // 循环写文字
+      while (alreadyCount < textLength) {
+        // alreadyCount - start + 1 -> 当前摘取出来的文字
+        // 比较可用宽度,寻找最大可写文字长度
+        while ((alreadyCount - start + 1) * preWidth < leftWidth && alreadyCount < textLength) {
+          alreadyCount++;
+        }
+
+        // 取出文字
+        let text = subView.text.substr(start, alreadyCount - start);
+
+        const y = -(height / 2) + subView.css.fontSize.toPx() + lineIndex * lineHeight;
+
+        // 设置文字样式
+        this.ctx.font = getStyle(subView.css);
+
+        this.ctx.fillStyle = subView.css.color || 'black';
+        this.ctx.textAlign = 'left';
+
+        // 执行画布操作
+        if (subView.css.textStyle === 'stroke') {
+          this.ctx.strokeText(text, x, y);
+        } else {
+          this.ctx.fillText(text, x, y);
+        }
+
+        // 当次已使用宽度
+        let currentUsedWidth = this.ctx.measureText(text).width;
+
+        const fontSize = subView.css.fontSize.toPx();
+
+        // 画 textDecoration
+        let textDecoration;
+        if (subView.css.textDecoration) {
+          this.ctx.lineWidth = fontSize / 13;
+          this.ctx.beginPath();
+          if (/\bunderline\b/.test(subView.css.textDecoration)) {
+            this.ctx.moveTo(x, y);
+            this.ctx.lineTo(x + currentUsedWidth, y);
+            textDecoration = {
+              moveTo: [x, y],
+              lineTo: [x + currentUsedWidth, y],
+            };
+          }
+          if (/\boverline\b/.test(subView.css.textDecoration)) {
+            this.ctx.moveTo(x, y - fontSize);
+            this.ctx.lineTo(x + currentUsedWidth, y - fontSize);
+            textDecoration = {
+              moveTo: [x, y - fontSize],
+              lineTo: [x + currentUsedWidth, y - fontSize],
+            };
+          }
+          if (/\bline-through\b/.test(subView.css.textDecoration)) {
+            this.ctx.moveTo(x, y - fontSize / 3);
+            this.ctx.lineTo(x + currentUsedWidth, y - fontSize / 3);
+            textDecoration = {
+              moveTo: [x, y - fontSize / 3],
+              lineTo: [x + currentUsedWidth, y - fontSize / 3],
+            };
+          }
+          this.ctx.closePath();
+          this.ctx.strokeStyle = subView.css.color;
+          this.ctx.stroke();
+        }
+
+        // 重置数据
+        start = alreadyCount;
+        leftWidth -= currentUsedWidth;
+        x += currentUsedWidth;
+        // 如果剩余宽度 小于等于0 或者小于一个字的平均宽度,换行
+        if (leftWidth <= 0 || leftWidth < preWidth) {
+          leftWidth = width;
+          x = staticX;
+          lineIndex++;
+        }
+      }
+    }
+
+    this.ctx.restore();
+    this._doBorder(view, width, height);
+  }
+
+  _fillAbsText(view) {
+    if (!view.text) {
+      return;
+    }
+    if (view.css.background) {
+      // 生成背景
+      this._doBackground(view);
+    }
+    this.ctx.save();
+    const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius);
+    this.ctx.fillStyle = view.css.color || 'black';
+    if (view.id && penCache.textLines[view.id]) {
+      this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
+      for (const i of penCache.textLines[view.id]) {
+        const { measuredWith, text, x, y, textDecoration } = i;
+        if (view.css.textStyle === 'stroke') {
+          this.ctx.strokeText(text, x, y, measuredWith);
+        } else {
+          this.ctx.fillText(text, x, y, measuredWith);
+        }
+        if (textDecoration) {
+          const fontSize = view.css.fontSize.toPx();
+          this.ctx.lineWidth = fontSize / 13;
+          this.ctx.beginPath();
+          this.ctx.moveTo(...textDecoration.moveTo);
+          this.ctx.lineTo(...textDecoration.lineTo);
+          this.ctx.closePath();
+          this.ctx.strokeStyle = view.css.color;
+          this.ctx.stroke();
+        }
+      }
+    } else {
+      const { lines, lineHeight, textArray, linesArray } = extra;
+      // 如果设置了id,则保留 text 的长度
+      if (view.id) {
+        let textWidth = 0;
+        for (let i = 0; i < textArray.length; ++i) {
+          const _w = this.ctx.measureText(textArray[i]).width;
+          textWidth = _w > textWidth ? _w : textWidth;
+        }
+        penCache.viewRect[view.id].width = width ? (textWidth < width ? textWidth : width) : textWidth;
+      }
+      let lineIndex = 0;
+      for (let j = 0; j < textArray.length; ++j) {
+        const preLineLength = Math.ceil(textArray[j].length / linesArray[j]);
+        let start = 0;
+        let alreadyCount = 0;
+
+        for (let i = 0; i < linesArray[j]; ++i) {
+          // 绘制行数大于最大行数,则直接跳出循环
+          if (lineIndex >= lines) {
+            break;
+          }
+          alreadyCount = preLineLength;
+          let text = textArray[j].substr(start, alreadyCount);
+          let measuredWith = this.ctx.measureText(text).width;
+          // 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除
+          // 如果已经到文本末尾,也不要进行该循环
+          while (
+            start + alreadyCount <= textArray[j].length &&
+            (width - measuredWith > view.css.fontSize.toPx() || measuredWith - width > view.css.fontSize.toPx())
+          ) {
+            if (measuredWith < width) {
+              text = textArray[j].substr(start, ++alreadyCount);
+            } else {
+              if (text.length <= 1) {
+                // 如果只有一个字符时,直接跳出循环
+                break;
+              }
+              text = textArray[j].substr(start, --alreadyCount);
+              // break;
+            }
+            measuredWith = this.ctx.measureText(text).width;
+          }
+          start += text.length;
+          // 如果是最后一行了,发现还有未绘制完的内容,则加...
+          if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) {
+            while (this.ctx.measureText(`${text}...`).width > width) {
+              if (text.length <= 1) {
+                // 如果只有一个字符时,直接跳出循环
+                break;
+              }
+              text = text.substring(0, text.length - 1);
+            }
+            text += '...';
+            measuredWith = this.ctx.measureText(text).width;
+          }
+          this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
+          let x;
+          let lineX;
+          switch (view.css.textAlign) {
+            case 'center':
+              x = 0;
+              lineX = x - measuredWith / 2;
+              break;
+            case 'right':
+              x = width / 2;
+              lineX = x - measuredWith;
+              break;
+            default:
+              x = -(width / 2);
+              lineX = x;
+              break;
+          }
+
+          const y =
+            -(height / 2) +
+            (lineIndex === 0 ? view.css.fontSize.toPx() : view.css.fontSize.toPx() + lineIndex * lineHeight);
+          lineIndex++;
+          if (view.css.textStyle === 'stroke') {
+            this.ctx.strokeText(text, x, y, measuredWith);
+          } else {
+            this.ctx.fillText(text, x, y, measuredWith);
+          }
+          const fontSize = view.css.fontSize.toPx();
+          let textDecoration;
+          if (view.css.textDecoration) {
+            this.ctx.lineWidth = fontSize / 13;
+            this.ctx.beginPath();
+            if (/\bunderline\b/.test(view.css.textDecoration)) {
+              this.ctx.moveTo(lineX, y);
+              this.ctx.lineTo(lineX + measuredWith, y);
+              textDecoration = {
+                moveTo: [lineX, y],
+                lineTo: [lineX + measuredWith, y],
+              };
+            }
+            if (/\boverline\b/.test(view.css.textDecoration)) {
+              this.ctx.moveTo(lineX, y - fontSize);
+              this.ctx.lineTo(lineX + measuredWith, y - fontSize);
+              textDecoration = {
+                moveTo: [lineX, y - fontSize],
+                lineTo: [lineX + measuredWith, y - fontSize],
+              };
+            }
+            if (/\bline-through\b/.test(view.css.textDecoration)) {
+              this.ctx.moveTo(lineX, y - fontSize / 3);
+              this.ctx.lineTo(lineX + measuredWith, y - fontSize / 3);
+              textDecoration = {
+                moveTo: [lineX, y - fontSize / 3],
+                lineTo: [lineX + measuredWith, y - fontSize / 3],
+              };
+            }
+            this.ctx.closePath();
+            this.ctx.strokeStyle = view.css.color;
+            this.ctx.stroke();
+          }
+          if (view.id) {
+            penCache.textLines[view.id]
+              ? penCache.textLines[view.id].push({
+                  text,
+                  x,
+                  y,
+                  measuredWith,
+                  textDecoration,
+                })
+              : (penCache.textLines[view.id] = [
+                  {
+                    text,
+                    x,
+                    y,
+                    measuredWith,
+                    textDecoration,
+                  },
+                ]);
+          }
+        }
+      }
+    }
+    this.ctx.restore();
+    this._doBorder(view, width, height);
+  }
+
+  _drawAbsRect(view) {
+    this.ctx.save();
+    const { width, height } = this._preProcess(view);
+    if (GD.api.isGradient(view.css.color)) {
+      GD.api.doGradient(view.css.color, width, height, this.ctx);
+    } else {
+      this.ctx.fillStyle = view.css.color;
+    }
+    const { borderRadius, borderStyle, borderWidth } = view.css;
+    this._border({
+      borderRadius,
+      width,
+      height,
+      borderWidth,
+      borderStyle,
+    });
+    this.ctx.fill();
+    this.ctx.restore();
+    this._doBorder(view, width, height);
+  }
+
+  // shadow 支持 (x, y, blur, color), 不支持 spread
+  // shadow:0px 0px 10px rgba(0,0,0,0.1);
+  _doShadow(view) {
+    if (!view.css || !view.css.shadow) {
+      return;
+    }
+    const box = view.css.shadow.replace(/,\s+/g, ',').split(/\s+/);
+    if (box.length > 4) {
+      console.error("shadow don't spread option");
+      return;
+    }
+    this.ctx.shadowOffsetX = parseInt(box[0], 10);
+    this.ctx.shadowOffsetY = parseInt(box[1], 10);
+    this.ctx.shadowBlur = parseInt(box[2], 10);
+    this.ctx.shadowColor = box[3];
+  }
+
+  _getAngle(angle) {
+    return (Number(angle) * Math.PI) / 180;
+  }
+}

+ 784 - 0
componets/painter/lib/qrcode.js

@@ -0,0 +1,784 @@
+/* eslint-disable */
+!(function () {
+
+  // alignment pattern
+  var adelta = [
+    0, 11, 15, 19, 23, 27, 31,
+    16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
+    26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
+  ];
+
+  // version block
+  var vpat = [
+    0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
+    0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
+    0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
+    0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
+    0x541, 0xc69
+  ];
+
+  // final format bits with mask: level << 3 | mask
+  var fmtword = [
+    0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976,    //L
+    0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0,    //M
+    0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed,    //Q
+    0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b    //H
+  ];
+
+  // 4 per version: number of blocks 1,2; data width; ecc width
+  var eccblocks = [
+    1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17,
+    1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28,
+    1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22,
+    1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16,
+    1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22,
+    2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28,
+    2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26,
+    2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26,
+    2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24,
+    2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28,
+    4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24,
+    2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28,
+    4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22,
+    3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24,
+    5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24,
+    5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30,
+    1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28,
+    5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28,
+    3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26,
+    3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
+    4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30,
+    2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24,
+    4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
+    6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
+    8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
+    10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
+    8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
+    3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
+    7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
+    5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
+    13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
+    17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
+    17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
+    13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
+    12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
+    6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
+    17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
+    4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
+    20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
+    19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
+  ];
+
+  // Galois field log table
+  var glog = [
+    0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
+    0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
+    0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
+    0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
+    0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
+    0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
+    0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
+    0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
+    0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
+    0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
+    0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
+    0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
+    0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
+    0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
+    0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
+    0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
+  ];
+
+  // Galios field exponent table
+  var gexp = [
+    0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
+    0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
+    0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
+    0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
+    0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
+    0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
+    0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
+    0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
+    0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
+    0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
+    0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
+    0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
+    0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
+    0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
+    0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
+    0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
+  ];
+
+  // Working buffers:
+  // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
+  var strinbuf = [], eccbuf = [], qrframe = [], framask = [], rlens = [];
+  // Control values - width is based on version, last 4 are from table.
+  var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
+  var ecclevel = 2;
+  // set bit to indicate cell in qrframe is immutable.  symmetric around diagonal
+  function setmask(x, y) {
+    var bt;
+    if (x > y) {
+      bt = x;
+      x = y;
+      y = bt;
+    }
+    // y*y = 1+3+5...
+    bt = y;
+    bt *= y;
+    bt += y;
+    bt >>= 1;
+    bt += x;
+    framask[bt] = 1;
+  }
+
+  // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
+  function putalign(x, y) {
+    var j;
+
+    qrframe[x + width * y] = 1;
+    for (j = -2; j < 2; j++) {
+      qrframe[(x + j) + width * (y - 2)] = 1;
+      qrframe[(x - 2) + width * (y + j + 1)] = 1;
+      qrframe[(x + 2) + width * (y + j)] = 1;
+      qrframe[(x + j + 1) + width * (y + 2)] = 1;
+    }
+    for (j = 0; j < 2; j++) {
+      setmask(x - 1, y + j);
+      setmask(x + 1, y - j);
+      setmask(x - j, y - 1);
+      setmask(x + j, y + 1);
+    }
+  }
+
+  //========================================================================
+  // Reed Solomon error correction
+  // exponentiation mod N
+  function modnn(x) {
+    while (x >= 255) {
+      x -= 255;
+      x = (x >> 8) + (x & 255);
+    }
+    return x;
+  }
+
+  var genpoly = [];
+
+  // Calculate and append ECC data to data block.  Block is in strinbuf, indexes to buffers given.
+  function appendrs(data, dlen, ecbuf, eclen) {
+    var i, j, fb;
+
+    for (i = 0; i < eclen; i++)
+      strinbuf[ecbuf + i] = 0;
+    for (i = 0; i < dlen; i++) {
+      fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
+      if (fb != 255)     /* fb term is non-zero */
+        for (j = 1; j < eclen; j++)
+          strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
+      else
+        for (j = ecbuf; j < ecbuf + eclen; j++)
+          strinbuf[j] = strinbuf[j + 1];
+      strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
+    }
+  }
+
+  //========================================================================
+  // Frame data insert following the path rules
+
+  // check mask - since symmetrical use half.
+  function ismasked(x, y) {
+    var bt;
+    if (x > y) {
+      bt = x;
+      x = y;
+      y = bt;
+    }
+    bt = y;
+    bt += y * y;
+    bt >>= 1;
+    bt += x;
+    return framask[bt];
+  }
+
+  //========================================================================
+  //  Apply the selected mask out of the 8.
+  function applymask(m) {
+    var x, y, r3x, r3y;
+
+    switch (m) {
+      case 0:
+        for (y = 0; y < width; y++)
+          for (x = 0; x < width; x++)
+            if (!((x + y) & 1) && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+        break;
+      case 1:
+        for (y = 0; y < width; y++)
+          for (x = 0; x < width; x++)
+            if (!(y & 1) && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+        break;
+      case 2:
+        for (y = 0; y < width; y++)
+          for (r3x = 0, x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3)
+              r3x = 0;
+            if (!r3x && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        break;
+      case 3:
+        for (r3y = 0, y = 0; y < width; y++ , r3y++) {
+          if (r3y == 3)
+            r3y = 0;
+          for (r3x = r3y, x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3)
+              r3x = 0;
+            if (!r3x && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        }
+        break;
+      case 4:
+        for (y = 0; y < width; y++)
+          for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3) {
+              r3x = 0;
+              r3y = !r3y;
+            }
+            if (!r3y && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        break;
+      case 5:
+        for (r3y = 0, y = 0; y < width; y++ , r3y++) {
+          if (r3y == 3)
+            r3y = 0;
+          for (r3x = 0, x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3)
+              r3x = 0;
+            if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        }
+        break;
+      case 6:
+        for (r3y = 0, y = 0; y < width; y++ , r3y++) {
+          if (r3y == 3)
+            r3y = 0;
+          for (r3x = 0, x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3)
+              r3x = 0;
+            if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        }
+        break;
+      case 7:
+        for (r3y = 0, y = 0; y < width; y++ , r3y++) {
+          if (r3y == 3)
+            r3y = 0;
+          for (r3x = 0, x = 0; x < width; x++ , r3x++) {
+            if (r3x == 3)
+              r3x = 0;
+            if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
+              qrframe[x + y * width] ^= 1;
+          }
+        }
+        break;
+    }
+    return;
+  }
+
+  // Badness coefficients.
+  var N1 = 3, N2 = 3, N3 = 40, N4 = 10;
+
+  // Using the table of the length of each run, calculate the amount of bad image 
+  // - long runs or those that look like finders; called twice, once each for X and Y
+  function badruns(length) {
+    var i;
+    var runsbad = 0;
+    for (i = 0; i <= length; i++)
+      if (rlens[i] >= 5)
+        runsbad += N1 + rlens[i] - 5;
+    // BwBBBwB as in finder
+    for (i = 3; i < length - 1; i += 2)
+      if (rlens[i - 2] == rlens[i + 2]
+        && rlens[i + 2] == rlens[i - 1]
+        && rlens[i - 1] == rlens[i + 1]
+        && rlens[i - 1] * 3 == rlens[i]
+        // white around the black pattern? Not part of spec
+        && (rlens[i - 3] == 0 // beginning
+          || i + 3 > length  // end
+          || rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
+      )
+        runsbad += N3;
+    return runsbad;
+  }
+
+  // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
+  function badcheck() {
+    var x, y, h, b, b1;
+    var thisbad = 0;
+    var bw = 0;
+
+    // blocks of same color.
+    for (y = 0; y < width - 1; y++)
+      for (x = 0; x < width - 1; x++)
+        if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y]
+          && qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
+          || !(qrframe[x + width * y] || qrframe[(x + 1) + width * y]
+            || qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
+          thisbad += N2;
+
+    // X runs
+    for (y = 0; y < width; y++) {
+      rlens[0] = 0;
+      for (h = b = x = 0; x < width; x++) {
+        if ((b1 = qrframe[x + width * y]) == b)
+          rlens[h]++;
+        else
+          rlens[++h] = 1;
+        b = b1;
+        bw += b ? 1 : -1;
+      }
+      thisbad += badruns(h);
+    }
+
+    // black/white imbalance
+    if (bw < 0)
+      bw = -bw;
+
+    var big = bw;
+    var count = 0;
+    big += big << 2;
+    big <<= 1;
+    while (big > width * width)
+      big -= width * width, count++;
+    thisbad += count * N4;
+
+    // Y runs
+    for (x = 0; x < width; x++) {
+      rlens[0] = 0;
+      for (h = b = y = 0; y < width; y++) {
+        if ((b1 = qrframe[x + width * y]) == b)
+          rlens[h]++;
+        else
+          rlens[++h] = 1;
+        b = b1;
+      }
+      thisbad += badruns(h);
+    }
+    return thisbad;
+  }
+
+  function genframe(instring) {
+    var x, y, k, t, v, i, j, m;
+
+    // find the smallest version that fits the string
+    t = instring.length;
+    version = 0;
+    do {
+      version++;
+      k = (ecclevel - 1) * 4 + (version - 1) * 16;
+      neccblk1 = eccblocks[k++];
+      neccblk2 = eccblocks[k++];
+      datablkw = eccblocks[k++];
+      eccblkwid = eccblocks[k];
+      k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
+      if (t <= k)
+        break;
+    } while (version < 40);
+
+    // FIXME - insure that it fits insted of being truncated
+    width = 17 + 4 * version;
+
+    // allocate, clear and setup data structures
+    v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+    for (t = 0; t < v; t++)
+      eccbuf[t] = 0;
+    strinbuf = instring.slice(0);
+
+    for (t = 0; t < width * width; t++)
+      qrframe[t] = 0;
+
+    for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
+      framask[t] = 0;
+
+    // insert finders - black to frame, white to mask
+    for (t = 0; t < 3; t++) {
+      k = 0;
+      y = 0;
+      if (t == 1)
+        k = (width - 7);
+      if (t == 2)
+        y = (width - 7);
+      qrframe[(y + 3) + width * (k + 3)] = 1;
+      for (x = 0; x < 6; x++) {
+        qrframe[(y + x) + width * k] = 1;
+        qrframe[y + width * (k + x + 1)] = 1;
+        qrframe[(y + 6) + width * (k + x)] = 1;
+        qrframe[(y + x + 1) + width * (k + 6)] = 1;
+      }
+      for (x = 1; x < 5; x++) {
+        setmask(y + x, k + 1);
+        setmask(y + 1, k + x + 1);
+        setmask(y + 5, k + x);
+        setmask(y + x + 1, k + 5);
+      }
+      for (x = 2; x < 4; x++) {
+        qrframe[(y + x) + width * (k + 2)] = 1;
+        qrframe[(y + 2) + width * (k + x + 1)] = 1;
+        qrframe[(y + 4) + width * (k + x)] = 1;
+        qrframe[(y + x + 1) + width * (k + 4)] = 1;
+      }
+    }
+
+    // alignment blocks
+    if (version > 1) {
+      t = adelta[version];
+      y = width - 7;
+      for (; ;) {
+        x = width - 7;
+        while (x > t - 3) {
+          putalign(x, y);
+          if (x < t)
+            break;
+          x -= t;
+        }
+        if (y <= t + 9)
+          break;
+        y -= t;
+        putalign(6, y);
+        putalign(y, 6);
+      }
+    }
+
+    // single black
+    qrframe[8 + width * (width - 8)] = 1;
+
+    // timing gap - mask only
+    for (y = 0; y < 7; y++) {
+      setmask(7, y);
+      setmask(width - 8, y);
+      setmask(7, y + width - 7);
+    }
+    for (x = 0; x < 8; x++) {
+      setmask(x, 7);
+      setmask(x + width - 8, 7);
+      setmask(x, width - 8);
+    }
+
+    // reserve mask-format area
+    for (x = 0; x < 9; x++)
+      setmask(x, 8);
+    for (x = 0; x < 8; x++) {
+      setmask(x + width - 8, 8);
+      setmask(8, x);
+    }
+    for (y = 0; y < 7; y++)
+      setmask(8, y + width - 7);
+
+    // timing row/col
+    for (x = 0; x < width - 14; x++)
+      if (x & 1) {
+        setmask(8 + x, 6);
+        setmask(6, 8 + x);
+      }
+      else {
+        qrframe[(8 + x) + width * 6] = 1;
+        qrframe[6 + width * (8 + x)] = 1;
+      }
+
+    // version block
+    if (version > 6) {
+      t = vpat[version - 7];
+      k = 17;
+      for (x = 0; x < 6; x++)
+        for (y = 0; y < 3; y++ , k--)
+          if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
+            qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
+            qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
+          }
+          else {
+            setmask(5 - x, 2 - y + width - 11);
+            setmask(2 - y + width - 11, 5 - x);
+          }
+    }
+
+    // sync mask bits - only set above for white spaces, so add in black bits
+    for (y = 0; y < width; y++)
+      for (x = 0; x <= y; x++)
+        if (qrframe[x + width * y])
+          setmask(x, y);
+
+    // convert string to bitstream
+    // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
+    v = strinbuf.length;
+
+    // string to array
+    for (i = 0; i < v; i++)
+      eccbuf[i] = strinbuf.charCodeAt(i);
+    strinbuf = eccbuf.slice(0);
+
+    // calculate max string length
+    x = datablkw * (neccblk1 + neccblk2) + neccblk2;
+    if (v >= x - 2) {
+      v = x - 2;
+      if (version > 9)
+        v--;
+    }
+
+    // shift and repack to insert length prefix
+    i = v;
+    if (version > 9) {
+      strinbuf[i + 2] = 0;
+      strinbuf[i + 3] = 0;
+      while (i--) {
+        t = strinbuf[i];
+        strinbuf[i + 3] |= 255 & (t << 4);
+        strinbuf[i + 2] = t >> 4;
+      }
+      strinbuf[2] |= 255 & (v << 4);
+      strinbuf[1] = v >> 4;
+      strinbuf[0] = 0x40 | (v >> 12);
+    }
+    else {
+      strinbuf[i + 1] = 0;
+      strinbuf[i + 2] = 0;
+      while (i--) {
+        t = strinbuf[i];
+        strinbuf[i + 2] |= 255 & (t << 4);
+        strinbuf[i + 1] = t >> 4;
+      }
+      strinbuf[1] |= 255 & (v << 4);
+      strinbuf[0] = 0x40 | (v >> 4);
+    }
+    // fill to end with pad pattern
+    i = v + 3 - (version < 10);
+    while (i < x) {
+      strinbuf[i++] = 0xec;
+      // buffer has room    if (i == x)      break;
+      strinbuf[i++] = 0x11;
+    }
+
+    // calculate and append ECC
+
+    // calculate generator polynomial
+    genpoly[0] = 1;
+    for (i = 0; i < eccblkwid; i++) {
+      genpoly[i + 1] = 1;
+      for (j = i; j > 0; j--)
+        genpoly[j] = genpoly[j]
+          ? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
+      genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
+    }
+    for (i = 0; i <= eccblkwid; i++)
+      genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
+
+    // append ecc to data buffer
+    k = x;
+    y = 0;
+    for (i = 0; i < neccblk1; i++) {
+      appendrs(y, datablkw, k, eccblkwid);
+      y += datablkw;
+      k += eccblkwid;
+    }
+    for (i = 0; i < neccblk2; i++) {
+      appendrs(y, datablkw + 1, k, eccblkwid);
+      y += datablkw + 1;
+      k += eccblkwid;
+    }
+    // interleave blocks
+    y = 0;
+    for (i = 0; i < datablkw; i++) {
+      for (j = 0; j < neccblk1; j++)
+        eccbuf[y++] = strinbuf[i + j * datablkw];
+      for (j = 0; j < neccblk2; j++)
+        eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+    }
+    for (j = 0; j < neccblk2; j++)
+      eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+    for (i = 0; i < eccblkwid; i++)
+      for (j = 0; j < neccblk1 + neccblk2; j++)
+        eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
+    strinbuf = eccbuf;
+
+    // pack bits into frame avoiding masked area.
+    x = y = width - 1;
+    k = v = 1;         // up, minus
+    /* inteleaved data and ecc codes */
+    m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+    for (i = 0; i < m; i++) {
+      t = strinbuf[i];
+      for (j = 0; j < 8; j++ , t <<= 1) {
+        if (0x80 & t)
+          qrframe[x + width * y] = 1;
+        do {        // find next fill position
+          if (v)
+            x--;
+          else {
+            x++;
+            if (k) {
+              if (y != 0)
+                y--;
+              else {
+                x -= 2;
+                k = !k;
+                if (x == 6) {
+                  x--;
+                  y = 9;
+                }
+              }
+            }
+            else {
+              if (y != width - 1)
+                y++;
+              else {
+                x -= 2;
+                k = !k;
+                if (x == 6) {
+                  x--;
+                  y -= 8;
+                }
+              }
+            }
+          }
+          v = !v;
+        } while (ismasked(x, y));
+      }
+    }
+
+    // save pre-mask copy of frame
+    strinbuf = qrframe.slice(0);
+    t = 0;           // best
+    y = 30000;         // demerit
+    // for instead of while since in original arduino code
+    // if an early mask was "good enough" it wouldn't try for a better one
+    // since they get more complex and take longer.
+    for (k = 0; k < 8; k++) {
+      applymask(k);      // returns black-white imbalance
+      x = badcheck();
+      if (x < y) { // current mask better than previous best?
+        y = x;
+        t = k;
+      }
+      if (t == 7)
+        break;       // don't increment i to a void redoing mask
+      qrframe = strinbuf.slice(0); // reset for next pass
+    }
+    if (t != k)         // redo best mask - none good enough, last wasn't t
+      applymask(t);
+
+    // add in final mask/ecclevel bytes
+    y = fmtword[t + ((ecclevel - 1) << 3)];
+    // low byte
+    for (k = 0; k < 8; k++ , y >>= 1)
+      if (y & 1) {
+        qrframe[(width - 1 - k) + width * 8] = 1;
+        if (k < 6)
+          qrframe[8 + width * k] = 1;
+        else
+          qrframe[8 + width * (k + 1)] = 1;
+      }
+    // high byte
+    for (k = 0; k < 7; k++ , y >>= 1)
+      if (y & 1) {
+        qrframe[8 + width * (width - 7 + k)] = 1;
+        if (k)
+          qrframe[(6 - k) + width * 8] = 1;
+        else
+          qrframe[7 + width * 8] = 1;
+      }
+    return qrframe;
+  }
+
+
+
+
+  var _canvas = null;
+
+  var api = {
+
+    get ecclevel() {
+      return ecclevel;
+    },
+
+    set ecclevel(val) {
+      ecclevel = val;
+    },
+
+    get size() {
+      return _size;
+    },
+
+    set size(val) {
+      _size = val
+    },
+
+    get canvas() {
+      return _canvas;
+    },
+
+    set canvas(el) {
+      _canvas = el;
+    },
+
+    getFrame: function (string) {
+      return genframe(string);
+    },
+    //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
+    utf16to8: function (str) {
+      var out, i, len, c;
+
+      out = "";
+      len = str.length;
+      for (i = 0; i < len; i++) {
+        c = str.charCodeAt(i);
+        if ((c >= 0x0001) && (c <= 0x007F)) {
+          out += str.charAt(i);
+        } else if (c > 0x07FF) {
+          out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+          out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+        } else {
+          out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+        }
+      }
+      return out;
+    },
+    /**
+     * 新增$this参数,传入组件的this,兼容在组件中生成
+     * @param bg 目前只能设置颜色值
+     */ 
+    draw: function (str, ctx, startX, startY, cavW, cavH, bg, color, $this, ecc) {
+      var that = this;
+      ecclevel = ecc || ecclevel;
+      if (!ctx) {
+        console.warn('No canvas provided to draw QR code in!')
+        return;
+      }
+      var size = Math.min(cavW, cavH);
+      str = that.utf16to8(str);//增加中文显示
+
+      var frame = that.getFrame(str);
+      var px = size / width;
+      if (bg) {
+        ctx.fillStyle = bg;
+        ctx.fillRect(startX, startY, cavW, cavW);
+      }
+      ctx.fillStyle = color || 'black';
+      for (var i = 0; i < width; i++) {
+        for (var j = 0; j < width; j++) {
+          if (frame[j * width + i]) {
+            ctx.fillRect(startX + px * i, startY + px * j, px, px);
+          }
+        }
+      }
+    }
+  }
+  module.exports = { api }
+  // exports.draw = api;
+
+})();

+ 97 - 0
componets/painter/lib/sha1.js

@@ -0,0 +1,97 @@
+var hexcase = 0;
+var chrsz = 8;
+
+function hex_sha1(s) {
+  return binb2hex(core_sha1(str2binb(s), s.length * chrsz));
+}
+
+function core_sha1(x, len) {
+  x[len >> 5] |= 0x80 << (24 - (len % 32));
+  x[(((len + 64) >> 9) << 4) + 15] = len;
+
+  var w = Array(80);
+  var a = 1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d = 271733878;
+  var e = -1009589776;
+
+  for (var i = 0; i < x.length; i += 16) {
+    var olda = a;
+    var oldb = b;
+    var oldc = c;
+    var oldd = d;
+    var olde = e;
+
+    for (var j = 0; j < 80; j++) {
+      if (j < 16) w[j] = x[i + j];
+      else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
+      var t = safe_add(
+        safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
+        safe_add(safe_add(e, w[j]), sha1_kt(j))
+      );
+      e = d;
+      d = c;
+      c = rol(b, 30);
+      b = a;
+      a = t;
+    }
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+    e = safe_add(e, olde);
+  }
+  return Array(a, b, c, d, e);
+}
+
+function sha1_ft(t, b, c, d) {
+  if (t < 20) return (b & c) | (~b & d);
+  if (t < 40) return b ^ c ^ d;
+  if (t < 60) return (b & c) | (b & d) | (c & d);
+  return b ^ c ^ d;
+}
+
+function sha1_kt(t) {
+  return t < 20
+    ? 1518500249
+    : t < 40
+    ? 1859775393
+    : t < 60
+    ? -1894007588
+    : -899497514;
+}
+
+function safe_add(x, y) {
+  var lsw = (x & 0xffff) + (y & 0xffff);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xffff);
+}
+
+function rol(num, cnt) {
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+function str2binb(str) {
+  var bin = Array();
+  var mask = (1 << chrsz) - 1;
+  for (var i = 0; i < str.length * chrsz; i += chrsz)
+    bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - (i % 32));
+  return bin;
+}
+
+function binb2hex(binarray) {
+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+  var str = "";
+  for (var i = 0; i < binarray.length * 4; i++) {
+    str +=
+      hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8 + 4)) & 0xf) +
+      hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8)) & 0xf);
+  }
+  return str;
+}
+
+module.exports = {
+  hex_sha1,
+}

+ 46 - 0
componets/painter/lib/string-polyfill.js

@@ -0,0 +1,46 @@
+String.prototype.substr = function (start, length) {
+  if (start === undefined) {
+    return this.toString()
+  }
+  if (typeof start !== 'number' || (typeof length !== 'number' && length !== undefined) ) {
+    return ''
+  }
+  const strArr = [...this]
+  const _length = strArr.length
+  if (_length + start < 0) {
+    start = 0
+  }
+  if (length === undefined || (start < 0 && start + length > 0)) {
+    return strArr.slice(start).join('')
+  } else {
+    return strArr.slice(start, start + length).join('')
+  }
+}
+
+
+String.prototype.substring = function (start, end) {
+  if (start === undefined) {
+    return this.toString()
+  }
+  if (typeof start !== 'number' || (typeof end !== 'number' && end !== undefined) ) {
+    return ''
+  }
+  if (!(start > 0)) {
+    start = 0
+  }
+  if (!(end > 0) && end !== undefined) {
+    end = 0
+  }
+  const strArr = [...this]
+  const _length = strArr.length
+  if (start > _length) {
+    start = _length
+  }
+  if (end > _length) {
+    end = _length
+  }
+  if (end < start) {
+    [start, end] = [end, start]
+  }
+  return strArr.slice(start, end).join('')
+}

+ 78 - 0
componets/painter/lib/util.js

@@ -0,0 +1,78 @@
+
+function isValidUrl(url) {
+  return isOnlineUrl(url) || isDataUrl(url);
+}
+
+function isOnlineUrl(url) {
+  return /((ht|f)tp(s?)|cloud):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url)
+}
+
+function isDataUrl(url) {
+  return /data:image\/(\w+);base64,(.*)/.test(url);
+}
+
+/**
+ * 深度对比两个对象是否一致
+ * from: https://github.com/epoberezkin/fast-deep-equal
+ * @param  {Object} a 对象a
+ * @param  {Object} b 对象b
+ * @return {Boolean}   是否相同
+ */
+/* eslint-disable */
+function equal(a, b) {
+  if (a === b) return true;
+
+  if (a && b && typeof a == 'object' && typeof b == 'object') {
+    var arrA = Array.isArray(a)
+      , arrB = Array.isArray(b)
+      , i
+      , length
+      , key;
+
+    if (arrA && arrB) {
+      length = a.length;
+      if (length != b.length) return false;
+      for (i = length; i-- !== 0;)
+        if (!equal(a[i], b[i])) return false;
+      return true;
+    }
+
+    if (arrA != arrB) return false;
+
+    var dateA = a instanceof Date
+      , dateB = b instanceof Date;
+    if (dateA != dateB) return false;
+    if (dateA && dateB) return a.getTime() == b.getTime();
+
+    var regexpA = a instanceof RegExp
+      , regexpB = b instanceof RegExp;
+    if (regexpA != regexpB) return false;
+    if (regexpA && regexpB) return a.toString() == b.toString();
+
+    var keys = Object.keys(a);
+    length = keys.length;
+
+    if (length !== Object.keys(b).length)
+      return false;
+
+    for (i = length; i-- !== 0;)
+      if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
+
+    for (i = length; i-- !== 0;) {
+      key = keys[i];
+      if (!equal(a[key], b[key])) return false;
+    }
+
+    return true;
+  }
+
+  return a!==a && b!==b;
+}
+
+module.exports = {
+  isValidUrl,
+  isOnlineUrl,
+  isDataUrl,
+  equal
+};
+

+ 619 - 0
componets/painter/lib/wx-canvas.js

@@ -0,0 +1,619 @@
+// @ts-check
+export default class WxCanvas {
+  ctx;
+  type;
+  canvasId;
+  canvasNode;
+  stepList = [];
+  canvasPrototype = {};
+
+  constructor(type, ctx, canvasId, isNew, canvasNode) {
+    this.ctx = ctx;
+    this.canvasId = canvasId;
+    this.type = type;
+    if (isNew) {
+      this.canvasNode = canvasNode || {};
+    }
+  }
+
+  set width(w) {
+    if (this.canvasNode) {
+      this.canvasNode.width = w;
+      // 经测试,在 2d 接口中如果不设置这个值,IOS 端有一定几率会出现图片显示不全的情况。
+      this.canvasNode._width = w;
+    }
+  }
+
+  get width() {
+    if (this.canvasNode) return this.canvasNode.width;
+    return 0;
+  }
+
+  set height(h) {
+    if (this.canvasNode) {
+      this.canvasNode.height = h;
+      // 经测试,在 2d 接口中如果不设置这个值,IOS 端有一定几率会出现图片显示不全的情况。
+      this.canvasNode._height = h;
+    }
+  }
+
+  get height() {
+    if (this.canvasNode) return this.canvasNode.height;
+    return 0;
+  }
+
+  set lineWidth(args) {
+    this.canvasPrototype.lineWidth = args;
+    this.stepList.push({
+      action: "lineWidth",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get lineWidth() {
+    return this.canvasPrototype.lineWidth;
+  }
+
+  set lineCap(args) {
+    this.canvasPrototype.lineCap = args;
+    this.stepList.push({
+      action: "lineCap",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get lineCap() {
+    return this.canvasPrototype.lineCap;
+  }
+
+  set lineJoin(args) {
+    this.canvasPrototype.lineJoin = args;
+    this.stepList.push({
+      action: "lineJoin",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get lineJoin() {
+    return this.canvasPrototype.lineJoin;
+  }
+
+  set miterLimit(args) {
+    this.canvasPrototype.miterLimit = args;
+    this.stepList.push({
+      action: "miterLimit",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get miterLimit() {
+    return this.canvasPrototype.miterLimit;
+  }
+
+  set lineDashOffset(args) {
+    this.canvasPrototype.lineDashOffset = args;
+    this.stepList.push({
+      action: "lineDashOffset",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get lineDashOffset() {
+    return this.canvasPrototype.lineDashOffset;
+  }
+
+  set font(args) {
+    this.canvasPrototype.font = args;
+    this.ctx.font = args;
+    this.stepList.push({
+      action: "font",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get font() {
+    return this.canvasPrototype.font;
+  }
+
+  set textAlign(args) {
+    this.canvasPrototype.textAlign = args;
+    this.stepList.push({
+      action: "textAlign",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get textAlign() {
+    return this.canvasPrototype.textAlign;
+  }
+
+  set textBaseline(args) {
+    this.canvasPrototype.textBaseline = args;
+    this.stepList.push({
+      action: "textBaseline",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get textBaseline() {
+    return this.canvasPrototype.textBaseline;
+  }
+
+  set fillStyle(args) {
+    this.canvasPrototype.fillStyle = args;
+    this.stepList.push({
+      action: "fillStyle",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get fillStyle() {
+    return this.canvasPrototype.fillStyle;
+  }
+
+  set strokeStyle(args) {
+    this.canvasPrototype.strokeStyle = args;
+    this.stepList.push({
+      action: "strokeStyle",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get strokeStyle() {
+    return this.canvasPrototype.strokeStyle;
+  }
+
+  set globalAlpha(args) {
+    this.canvasPrototype.globalAlpha = args;
+    this.stepList.push({
+      action: "globalAlpha",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get globalAlpha() {
+    return this.canvasPrototype.globalAlpha;
+  }
+
+  set globalCompositeOperation(args) {
+    this.canvasPrototype.globalCompositeOperation = args;
+    this.stepList.push({
+      action: "globalCompositeOperation",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get globalCompositeOperation() {
+    return this.canvasPrototype.globalCompositeOperation;
+  }
+
+  set shadowColor(args) {
+    this.canvasPrototype.shadowColor = args;
+    this.stepList.push({
+      action: "shadowColor",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get shadowColor() {
+    return this.canvasPrototype.shadowColor;
+  }
+
+  set shadowOffsetX(args) {
+    this.canvasPrototype.shadowOffsetX = args;
+    this.stepList.push({
+      action: "shadowOffsetX",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get shadowOffsetX() {
+    return this.canvasPrototype.shadowOffsetX;
+  }
+
+  set shadowOffsetY(args) {
+    this.canvasPrototype.shadowOffsetY = args;
+    this.stepList.push({
+      action: "shadowOffsetY",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get shadowOffsetY() {
+    return this.canvasPrototype.shadowOffsetY;
+  }
+
+  set shadowBlur(args) {
+    this.canvasPrototype.shadowBlur = args;
+    this.stepList.push({
+      action: "shadowBlur",
+      args,
+      actionType: "set",
+    });
+  }
+
+  get shadowBlur() {
+    return this.canvasPrototype.shadowBlur;
+  }
+
+  save() {
+    this.stepList.push({
+      action: "save",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  restore() {
+    this.stepList.push({
+      action: "restore",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  setLineDash(...args) {
+    this.canvasPrototype.lineDash = args;
+    this.stepList.push({
+      action: "setLineDash",
+      args,
+      actionType: "func",
+    });
+  }
+
+  moveTo(...args) {
+    this.stepList.push({
+      action: "moveTo",
+      args,
+      actionType: "func",
+    });
+  }
+
+  closePath() {
+    this.stepList.push({
+      action: "closePath",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  lineTo(...args) {
+    this.stepList.push({
+      action: "lineTo",
+      args,
+      actionType: "func",
+    });
+  }
+
+  quadraticCurveTo(...args) {
+    this.stepList.push({
+      action: "quadraticCurveTo",
+      args,
+      actionType: "func",
+    });
+  }
+
+  bezierCurveTo(...args) {
+    this.stepList.push({
+      action: "bezierCurveTo",
+      args,
+      actionType: "func",
+    });
+  }
+
+  arcTo(...args) {
+    this.stepList.push({
+      action: "arcTo",
+      args,
+      actionType: "func",
+    });
+  }
+
+  arc(...args) {
+    this.stepList.push({
+      action: "arc",
+      args,
+      actionType: "func",
+    });
+  }
+
+  rect(...args) {
+    this.stepList.push({
+      action: "rect",
+      args,
+      actionType: "func",
+    });
+  }
+
+  scale(...args) {
+    this.stepList.push({
+      action: "scale",
+      args,
+      actionType: "func",
+    });
+  }
+
+  rotate(...args) {
+    this.stepList.push({
+      action: "rotate",
+      args,
+      actionType: "func",
+    });
+  }
+
+  translate(...args) {
+    this.stepList.push({
+      action: "translate",
+      args,
+      actionType: "func",
+    });
+  }
+
+  transform(...args) {
+    this.stepList.push({
+      action: "transform",
+      args,
+      actionType: "func",
+    });
+  }
+
+  setTransform(...args) {
+    this.stepList.push({
+      action: "setTransform",
+      args,
+      actionType: "func",
+    });
+  }
+
+  clearRect(...args) {
+    this.stepList.push({
+      action: "clearRect",
+      args,
+      actionType: "func",
+    });
+  }
+
+  fillRect(...args) {
+    this.stepList.push({
+      action: "fillRect",
+      args,
+      actionType: "func",
+    });
+  }
+
+  strokeRect(...args) {
+    this.stepList.push({
+      action: "strokeRect",
+      args,
+      actionType: "func",
+    });
+  }
+
+  fillText(...args) {
+    this.stepList.push({
+      action: "fillText",
+      args,
+      actionType: "func",
+    });
+  }
+
+  strokeText(...args) {
+    this.stepList.push({
+      action: "strokeText",
+      args,
+      actionType: "func",
+    });
+  }
+
+  beginPath() {
+    this.stepList.push({
+      action: "beginPath",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  fill() {
+    this.stepList.push({
+      action: "fill",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  stroke() {
+    this.stepList.push({
+      action: "stroke",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  drawFocusIfNeeded(...args) {
+    this.stepList.push({
+      action: "drawFocusIfNeeded",
+      args,
+      actionType: "func",
+    });
+  }
+
+  clip() {
+    this.stepList.push({
+      action: "clip",
+      args: null,
+      actionType: "func",
+    });
+  }
+
+  isPointInPath(...args) {
+    this.stepList.push({
+      action: "isPointInPath",
+      args,
+      actionType: "func",
+    });
+  }
+
+  drawImage(...args) {
+    this.stepList.push({
+      action: "drawImage",
+      args,
+      actionType: "func",
+    });
+  }
+
+  addHitRegion(...args) {
+    this.stepList.push({
+      action: "addHitRegion",
+      args,
+      actionType: "func",
+    });
+  }
+
+  removeHitRegion(...args) {
+    this.stepList.push({
+      action: "removeHitRegion",
+      args,
+      actionType: "func",
+    });
+  }
+
+  clearHitRegions(...args) {
+    this.stepList.push({
+      action: "clearHitRegions",
+      args,
+      actionType: "func",
+    });
+  }
+
+  putImageData(...args) {
+    this.stepList.push({
+      action: "putImageData",
+      args,
+      actionType: "func",
+    });
+  }
+
+  getLineDash() {
+    return this.canvasPrototype.lineDash;
+  }
+
+  createLinearGradient(...args) {
+    return this.ctx.createLinearGradient(...args);
+  }
+
+  createRadialGradient(...args) {
+    if (this.type === "2d") {
+      return this.ctx.createRadialGradient(...args);
+    } else {
+      return this.ctx.createCircularGradient(...args.slice(3, 6));
+    }
+  }
+
+  createPattern(...args) {
+    return this.ctx.createPattern(...args);
+  }
+
+  measureText(...args) {
+    return this.ctx.measureText(...args);
+  }
+
+  createImageData(...args) {
+    return this.ctx.createImageData(...args);
+  }
+
+  getImageData(...args) {
+    return this.ctx.getImageData(...args);
+  }
+
+  async draw(reserve, func) {
+    const realstepList = this.stepList.slice();
+    this.stepList.length = 0;
+    if (this.type === "mina") {
+      if (realstepList.length > 0) {
+        for (const step of realstepList) {
+          this.implementMinaStep(step);
+        }
+        realstepList.length = 0;
+      }
+      this.ctx.draw(reserve, func);
+    } else if (this.type === "2d") {
+      if (!reserve) {
+        this.ctx.clearRect(0, 0, this.canvasNode.width, this.canvasNode.height);
+      }
+      if (realstepList.length > 0) {
+        for (const step of realstepList) {
+          await this.implement2DStep(step);
+        }
+        realstepList.length = 0;
+      }
+      if (func) {
+        func();
+      }
+    }
+    realstepList.length = 0;
+  }
+
+  implementMinaStep(step) {
+    switch (step.action) {
+      case "textAlign": {
+        this.ctx.setTextAlign(step.args);
+        break;
+      }
+      case "textBaseline": {
+        this.ctx.setTextBaseline(step.args);
+        break;
+      }
+      default: {
+        if (step.actionType === "set") {
+          this.ctx[step.action] = step.args;
+        } else if (step.actionType === "func") {
+          if (step.args) {
+            this.ctx[step.action](...step.args);
+          } else {
+            this.ctx[step.action]();
+          }
+        }
+        break;
+      }
+    }
+  }
+
+  implement2DStep(step) {
+    return new Promise((resolve) => {
+      if (step.action === "drawImage") {
+        const img = this.canvasNode.createImage();
+        img.src = step.args[0];
+        img.onload = () => {
+          this.ctx.drawImage(img, ...step.args.slice(1));
+          resolve();
+        };
+      } else {
+        if (step.actionType === "set") {
+          this.ctx[step.action] = step.args;
+        } else if (step.actionType === "func") {
+          if (step.args) {
+            this.ctx[step.action](...step.args);
+          } else {
+            this.ctx[step.action]();
+          }
+        }
+        resolve();
+      }
+    });
+  }
+}

+ 869 - 0
componets/painter/painter.js

@@ -0,0 +1,869 @@
+import Pen, { penCache, clearPenCache } from './lib/pen';
+import Downloader from './lib/downloader';
+import WxCanvas from './lib/wx-canvas';
+
+const util = require('./lib/util');
+const calc = require('./lib/calc');
+
+const downloader = new Downloader();
+
+// 最大尝试的绘制次数
+const MAX_PAINT_COUNT = 5;
+const ACTION_DEFAULT_SIZE = 24;
+const ACTION_OFFSET = '2rpx';
+Component({
+  canvasWidthInPx: 0,
+  canvasHeightInPx: 0,
+  canvasNode: null,
+  paintCount: 0,
+  currentPalette: {},
+  outterDisabled: false,
+  isDisabled: false,
+  needClear: false,
+  /**
+   * 组件的属性列表
+   */
+  properties: {
+    use2D: {
+      type: Boolean,
+    },
+    customStyle: {
+      type: String,
+    },
+    // 运行自定义选择框和删除缩放按钮
+    customActionStyle: {
+      type: Object,
+    },
+    palette: {
+      type: Object,
+      observer: function (newVal, oldVal) {
+        if (this.isNeedRefresh(newVal, oldVal)) {
+          this.paintCount = 0;
+          clearPenCache();
+          this.startPaint();
+        }
+      },
+    },
+    dancePalette: {
+      type: Object,
+      observer: function (newVal, oldVal) {
+        if (!this.isEmpty(newVal) && !this.properties.use2D) {
+          clearPenCache();
+          this.initDancePalette(newVal);
+        }
+      },
+    },
+    // 缩放比,会在传入的 palette 中统一乘以该缩放比
+    scaleRatio: {
+      type: Number,
+      value: 1,
+    },
+    widthPixels: {
+      type: Number,
+      value: 0,
+    },
+    // 启用脏检查,默认 false
+    dirty: {
+      type: Boolean,
+      value: false,
+    },
+    LRU: {
+      type: Boolean,
+      value: false,
+    },
+    action: {
+      type: Object,
+      observer: function (newVal, oldVal) {
+        if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
+          this.doAction(newVal, null, false, true);
+        }
+      },
+    },
+    disableAction: {
+      type: Boolean,
+      observer: function (isDisabled) {
+        this.outterDisabled = isDisabled;
+        this.isDisabled = isDisabled;
+      },
+    },
+    clearActionBox: {
+      type: Boolean,
+      observer: function (needClear) {
+        if (needClear && !this.needClear) {
+          if (this.frontContext) {
+            setTimeout(() => {
+              this.frontContext.draw();
+            }, 100);
+            this.touchedView = {};
+            this.prevFindedIndex = this.findedIndex;
+            this.findedIndex = -1;
+          }
+        }
+        this.needClear = needClear;
+      },
+    },
+  },
+
+  data: {
+    picURL: '',
+    showCanvas: true,
+    painterStyle: '',
+  },
+
+  methods: {
+    /**
+     * 判断一个 object 是否为 空
+     * @param {object} object
+     */
+    isEmpty(object) {
+      for (const i in object) {
+        return false;
+      }
+      return true;
+    },
+
+    isNeedRefresh(newVal, oldVal) {
+      if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
+        return false;
+      }
+      return true;
+    },
+
+    getBox(rect, type) {
+      const boxArea = {
+        type: 'rect',
+        css: {
+          height: `${rect.bottom - rect.top}px`,
+          width: `${rect.right - rect.left}px`,
+          left: `${rect.left}px`,
+          top: `${rect.top}px`,
+          borderWidth: '4rpx',
+          borderColor: '#1A7AF8',
+          color: 'transparent',
+        },
+      };
+      if (type === 'text') {
+        boxArea.css = Object.assign({}, boxArea.css, {
+          borderStyle: 'dashed',
+        });
+      }
+      if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
+        boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border);
+      }
+      Object.assign(boxArea, {
+        id: 'box',
+      });
+      return boxArea;
+    },
+
+    getScaleIcon(rect, type) {
+      let scaleArea = {};
+      const { customActionStyle } = this.properties;
+      if (customActionStyle && customActionStyle.scale) {
+        scaleArea = {
+          type: 'image',
+          url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
+          css: {
+            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
+          },
+        };
+      } else {
+        scaleArea = {
+          type: 'rect',
+          css: {
+            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
+            color: '#0000ff',
+          },
+        };
+      }
+      scaleArea.css = Object.assign({}, scaleArea.css, {
+        align: 'center',
+        left: `${rect.right + ACTION_OFFSET.toPx()}px`,
+        top:
+          type === 'text'
+            ? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
+            : `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`,
+      });
+      Object.assign(scaleArea, {
+        id: 'scale',
+      });
+      return scaleArea;
+    },
+
+    getDeleteIcon(rect) {
+      let deleteArea = {};
+      const { customActionStyle } = this.properties;
+      if (customActionStyle && customActionStyle.scale) {
+        deleteArea = {
+          type: 'image',
+          url: customActionStyle.delete.icon,
+          css: {
+            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
+          },
+        };
+      } else {
+        deleteArea = {
+          type: 'rect',
+          css: {
+            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
+            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
+            color: '#0000ff',
+          },
+        };
+      }
+      deleteArea.css = Object.assign({}, deleteArea.css, {
+        align: 'center',
+        left: `${rect.left - ACTION_OFFSET.toPx()}px`,
+        top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`,
+      });
+      Object.assign(deleteArea, {
+        id: 'delete',
+      });
+      return deleteArea;
+    },
+
+    doAction(action, callback, isMoving, overwrite) {
+      if (this.properties.use2D) {
+        return;
+      }
+      let newVal = null;
+      if (action) {
+        newVal = action.view;
+      }
+      if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
+        // 带 id 的动作给撤回时使用,不带 id,表示对当前选中对象进行操作
+        const { views } = this.currentPalette;
+        for (let i = 0; i < views.length; i++) {
+          if (views[i].id === newVal.id) {
+            // 跨层回撤,需要重新构建三层关系
+            this.touchedView = views[i];
+            this.findedIndex = i;
+            this.sliceLayers();
+            break;
+          }
+        }
+      }
+
+      const doView = this.touchedView;
+
+      if (!doView || this.isEmpty(doView)) {
+        return;
+      }
+      if (newVal && newVal.css) {
+        if (overwrite) {
+          doView.css = newVal.css;
+        } else if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
+          doView.css = Object.assign({}, ...doView.css, ...newVal.css);
+        } else if (Array.isArray(doView.css)) {
+          doView.css = Object.assign({}, ...doView.css, newVal.css);
+        } else if (Array.isArray(newVal.css)) {
+          doView.css = Object.assign({}, doView.css, ...newVal.css);
+        } else {
+          doView.css = Object.assign({}, doView.css, newVal.css);
+        }
+      }
+      if (newVal && newVal.rect) {
+        doView.rect = newVal.rect;
+      }
+      if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
+        downloader
+          .download(newVal.url, this.properties.LRU)
+          .then(path => {
+            if (newVal.url.startsWith('https')) {
+              doView.originUrl = newVal.url;
+            }
+            doView.url = path;
+            wx.getImageInfo({
+              src: path,
+              success: res => {
+                doView.sHeight = res.height;
+                doView.sWidth = res.width;
+                this.reDraw(doView, callback, isMoving);
+              },
+              fail: () => {
+                this.reDraw(doView, callback, isMoving);
+              },
+            });
+          })
+          .catch(error => {
+            // 未下载成功,直接绘制
+            console.error(error);
+            this.reDraw(doView, callback, isMoving);
+          });
+      } else {
+        newVal && newVal.text && doView.text && newVal.text !== doView.text && (doView.text = newVal.text);
+        newVal &&
+          newVal.content &&
+          doView.content &&
+          newVal.content !== doView.content &&
+          (doView.content = newVal.content);
+        this.reDraw(doView, callback, isMoving);
+      }
+    },
+
+    reDraw(doView, callback, isMoving) {
+      const draw = {
+        width: this.currentPalette.width,
+        height: this.currentPalette.height,
+        views: this.isEmpty(doView) ? [] : [doView],
+      };
+      const pen = new Pen(this.globalContext, draw);
+
+      pen.paint(callbackInfo => {
+        callback && callback(callbackInfo);
+        this.triggerEvent('viewUpdate', {
+          view: this.touchedView,
+        });
+      });
+
+      const { rect, css, type } = doView;
+
+      this.block = {
+        width: this.currentPalette.width,
+        height: this.currentPalette.height,
+        views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)],
+      };
+      if (css && css.scalable) {
+        this.block.views.push(this.getScaleIcon(rect, type));
+      }
+      if (css && css.deletable) {
+        this.block.views.push(this.getDeleteIcon(rect));
+      }
+      const topBlock = new Pen(this.frontContext, this.block);
+      topBlock.paint();
+    },
+
+    isInView(x, y, rect) {
+      return x > rect.left && y > rect.top && x < rect.right && y < rect.bottom;
+    },
+
+    isInDelete(x, y) {
+      for (const view of this.block.views) {
+        if (view.id === 'delete') {
+          return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
+        }
+      }
+      return false;
+    },
+
+    isInScale(x, y) {
+      for (const view of this.block.views) {
+        if (view.id === 'scale') {
+          return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
+        }
+      }
+      return false;
+    },
+
+    touchedView: {},
+    findedIndex: -1,
+    onClick() {
+      const x = this.startX;
+      const y = this.startY;
+      const totalLayerCount = this.currentPalette.views.length;
+      let canBeTouched = [];
+      let isDelete = false;
+      let deleteIndex = -1;
+      for (let i = totalLayerCount - 1; i >= 0; i--) {
+        const view = this.currentPalette.views[i];
+        const { rect } = view;
+        if (this.touchedView && this.touchedView.id && this.touchedView.id === view.id && this.isInDelete(x, y, rect)) {
+          canBeTouched.length = 0;
+          deleteIndex = i;
+          isDelete = true;
+          break;
+        }
+        if (this.isInView(x, y, rect)) {
+          canBeTouched.push({
+            view,
+            index: i,
+          });
+        }
+      }
+      this.touchedView = {};
+      if (canBeTouched.length === 0) {
+        this.findedIndex = -1;
+      } else {
+        let i = 0;
+        const touchAble = canBeTouched.filter(item => Boolean(item.view.id));
+        if (touchAble.length === 0) {
+          this.findedIndex = canBeTouched[0].index;
+        } else {
+          for (i = 0; i < touchAble.length; i++) {
+            if (this.findedIndex === touchAble[i].index) {
+              i++;
+              break;
+            }
+          }
+          if (i === touchAble.length) {
+            i = 0;
+          }
+          this.touchedView = touchAble[i].view;
+          this.findedIndex = touchAble[i].index;
+          this.triggerEvent('viewClicked', {
+            view: this.touchedView,
+          });
+        }
+      }
+      if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
+        // 证明点击了背景 或无法移动的view
+        this.frontContext.draw();
+        if (isDelete) {
+          this.triggerEvent('touchEnd', {
+            view: this.currentPalette.views[deleteIndex],
+            index: deleteIndex,
+            type: 'delete',
+          });
+          this.doAction();
+        } else if (this.findedIndex < 0) {
+          this.triggerEvent('viewClicked', {});
+        }
+        this.findedIndex = -1;
+        this.prevFindedIndex = -1;
+      } else if (this.touchedView && this.touchedView.id) {
+        this.sliceLayers();
+      }
+    },
+
+    sliceLayers() {
+      const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex);
+      const topLayers = this.currentPalette.views.slice(this.findedIndex + 1);
+      const bottomDraw = {
+        width: this.currentPalette.width,
+        height: this.currentPalette.height,
+        background: this.currentPalette.background,
+        views: bottomLayers,
+      };
+      const topDraw = {
+        width: this.currentPalette.width,
+        height: this.currentPalette.height,
+        views: topLayers,
+      };
+      if (this.prevFindedIndex < this.findedIndex) {
+        new Pen(this.bottomContext, bottomDraw).paint();
+        this.doAction();
+        new Pen(this.topContext, topDraw).paint();
+      } else {
+        new Pen(this.topContext, topDraw).paint();
+        this.doAction();
+        new Pen(this.bottomContext, bottomDraw).paint();
+      }
+      this.prevFindedIndex = this.findedIndex;
+    },
+
+    startX: 0,
+    startY: 0,
+    startH: 0,
+    startW: 0,
+    isScale: false,
+    startTimeStamp: 0,
+    onTouchStart(event) {
+      if (this.isDisabled) {
+        return;
+      }
+      const { x, y } = event.touches[0];
+      this.startX = x;
+      this.startY = y;
+      this.startTimeStamp = new Date().getTime();
+      if (this.touchedView && !this.isEmpty(this.touchedView)) {
+        const { rect } = this.touchedView;
+        if (this.isInScale(x, y, rect)) {
+          this.isScale = true;
+          this.startH = rect.bottom - rect.top;
+          this.startW = rect.right - rect.left;
+        } else {
+          this.isScale = false;
+        }
+      } else {
+        this.isScale = false;
+      }
+    },
+
+    onTouchEnd(e) {
+      if (this.isDisabled) {
+        return;
+      }
+      const current = new Date().getTime();
+      if (current - this.startTimeStamp <= 500 && !this.hasMove) {
+        !this.isScale && this.onClick(e);
+      } else if (this.touchedView && !this.isEmpty(this.touchedView)) {
+        this.triggerEvent('touchEnd', {
+          view: this.touchedView,
+        });
+      }
+      this.hasMove = false;
+    },
+
+    onTouchCancel(e) {
+      if (this.isDisabled) {
+        return;
+      }
+      this.onTouchEnd(e);
+    },
+
+    hasMove: false,
+    onTouchMove(event) {
+      if (this.isDisabled) {
+        return;
+      }
+      this.hasMove = true;
+      if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
+        return;
+      }
+      const { x, y } = event.touches[0];
+      const offsetX = x - this.startX;
+      const offsetY = y - this.startY;
+      const { rect, type } = this.touchedView;
+      let css = {};
+      if (this.isScale) {
+        clearPenCache(this.touchedView.id);
+        const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1;
+        if (this.touchedView.css && this.touchedView.css.minWidth) {
+          if (newW < this.touchedView.css.minWidth.toPx()) {
+            return;
+          }
+        }
+        if (this.touchedView.rect && this.touchedView.rect.minWidth) {
+          if (newW < this.touchedView.rect.minWidth) {
+            return;
+          }
+        }
+        const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1;
+        css = {
+          width: `${newW}px`,
+        };
+        if (type !== 'text') {
+          if (type === 'image') {
+            css.height = `${(newW * this.startH) / this.startW}px`;
+          } else {
+            css.height = `${newH}px`;
+          }
+        }
+      } else {
+        this.startX = x;
+        this.startY = y;
+        css = {
+          left: `${rect.x + offsetX}px`,
+          top: `${rect.y + offsetY}px`,
+          right: undefined,
+          bottom: undefined,
+        };
+      }
+      this.doAction(
+        {
+          view: {
+            css,
+          },
+        },
+        null,
+        !this.isScale,
+      );
+    },
+
+    initScreenK() {
+      if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
+        try {
+          getApp().systemInfo = wx.getSystemInfoSync();
+        } catch (e) {
+          console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
+          return;
+        }
+      }
+      this.screenK = 0.5;
+      if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
+        this.screenK = getApp().systemInfo.screenWidth / 750;
+      }
+      setStringPrototype(this.screenK, this.properties.scaleRatio);
+    },
+
+    initDancePalette() {
+      if (this.properties.use2D) {
+        return;
+      }
+      this.isDisabled = true;
+      this.initScreenK();
+      this.downloadImages(this.properties.dancePalette).then(async palette => {
+        this.currentPalette = palette;
+        const { width, height } = palette;
+
+        if (!width || !height) {
+          console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
+          return;
+        }
+        this.setData({
+          painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
+        });
+        this.frontContext || (this.frontContext = await this.getCanvasContext(this.properties.use2D, 'front'));
+        this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
+        this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
+        this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
+        new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
+          this.isDisabled = false;
+          this.isDisabled = this.outterDisabled;
+          this.triggerEvent('didShow');
+        });
+        this.globalContext.draw();
+        this.frontContext.draw();
+        this.topContext.draw();
+      });
+      this.touchedView = {};
+    },
+
+    startPaint() {
+      this.initScreenK();
+      const { width, height } = this.properties.palette;
+
+      if (!width || !height) {
+        console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
+        return;
+      }
+
+      let needScale = false;
+      // 生成图片时,根据设置的像素值重新绘制
+      if (width.toPx() !== this.canvasWidthInPx) {
+        this.canvasWidthInPx = width.toPx();
+        needScale = this.properties.use2D;
+      }
+      if (this.properties.widthPixels) {
+        setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx);
+        this.canvasWidthInPx = this.properties.widthPixels;
+      }
+
+      if (this.canvasHeightInPx !== height.toPx()) {
+        this.canvasHeightInPx = height.toPx();
+        needScale = needScale || this.properties.use2D;
+      }
+      this.setData(
+        {
+          photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
+        },
+        function () {
+          this.downloadImages(this.properties.palette).then(async palette => {
+            if (!this.photoContext) {
+              this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
+            }
+            if (needScale) {
+              const scale = getApp().systemInfo.pixelRatio;
+              this.photoContext.width = this.canvasWidthInPx * scale;
+              this.photoContext.height = this.canvasHeightInPx * scale;
+              this.photoContext.scale(scale, scale);
+            }
+            new Pen(this.photoContext, palette).paint(() => {
+              this.saveImgToLocal();
+            });
+            setStringPrototype(this.screenK, this.properties.scaleRatio);
+          });
+        },
+      );
+    },
+
+    downloadImages(palette) {
+      return new Promise((resolve, reject) => {
+        let preCount = 0;
+        let completeCount = 0;
+        const paletteCopy = JSON.parse(JSON.stringify(palette));
+        if (paletteCopy.background) {
+          preCount++;
+          downloader.download(paletteCopy.background, this.properties.LRU).then(
+            path => {
+              paletteCopy.background = path;
+              completeCount++;
+              if (preCount === completeCount) {
+                resolve(paletteCopy);
+              }
+            },
+            () => {
+              completeCount++;
+              if (preCount === completeCount) {
+                resolve(paletteCopy);
+              }
+            },
+          );
+        }
+        if (paletteCopy.views) {
+          for (const view of paletteCopy.views) {
+            if (view && view.type === 'image' && view.url) {
+              preCount++;
+              /* eslint-disable no-loop-func */
+              downloader.download(view.url, this.properties.LRU).then(
+                path => {
+                  view.originUrl = view.url;
+                  view.url = path;
+                  wx.getImageInfo({
+                    src: path,
+                    success: res => {
+                      // 获得一下图片信息,供后续裁减使用
+                      view.sWidth = res.width;
+                      view.sHeight = res.height;
+                    },
+                    fail: error => {
+                      // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
+                      console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
+                      view.url = '';
+                    },
+                    complete: () => {
+                      completeCount++;
+                      if (preCount === completeCount) {
+                        resolve(paletteCopy);
+                      }
+                    },
+                  });
+                },
+                () => {
+                  completeCount++;
+                  if (preCount === completeCount) {
+                    resolve(paletteCopy);
+                  }
+                },
+              );
+            }
+          }
+        }
+        if (preCount === 0) {
+          resolve(paletteCopy);
+        }
+      });
+    },
+
+    saveImgToLocal() {
+      const that = this;
+      const optionsOf2d = {
+        canvas: that.canvasNode,
+      }
+      const optionsOfOld = {
+        canvasId: 'photo',
+        destWidth: that.canvasWidthInPx,
+        destHeight: that.canvasHeightInPx,
+      }
+      setTimeout(() => {
+        wx.canvasToTempFilePath(
+          {
+            ...(that.properties.use2D ? optionsOf2d : optionsOfOld),
+            success: function (res) {
+              that.getImageInfo(res.tempFilePath);
+            },
+            fail: function (error) {
+              console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
+              that.triggerEvent('imgErr', {
+                error: error,
+              });
+            },
+          },
+          this,
+        );
+      }, 300);
+    },
+
+    getCanvasContext(use2D, id) {
+      const that = this;
+      return new Promise(resolve => {
+        if (use2D) {
+          const query = wx.createSelectorQuery().in(that);
+          const selectId = `#${id}`;
+          query
+            .select(selectId)
+            .fields({ node: true, size: true })
+            .exec(res => {
+              that.canvasNode = res[0].node;
+              const ctx = that.canvasNode.getContext('2d');
+              const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
+              resolve(wxCanvas);
+            });
+        } else {
+          const temp = wx.createCanvasContext(id, that);
+          resolve(new WxCanvas('mina', temp, id, true));
+        }
+      });
+    },
+
+    getImageInfo(filePath) {
+      const that = this;
+      wx.getImageInfo({
+        src: filePath,
+        success: infoRes => {
+          if (that.paintCount > MAX_PAINT_COUNT) {
+            const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
+            console.error(error);
+            that.triggerEvent('imgErr', {
+              error: error,
+            });
+            return;
+          }
+          // 比例相符时才证明绘制成功,否则进行强制重绘制
+          if (
+            Math.abs(
+              (infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) /
+                (infoRes.height * that.canvasHeightInPx),
+            ) < 0.01
+          ) {
+            that.triggerEvent('imgOK', {
+              path: filePath,
+            });
+          } else {
+            that.startPaint();
+          }
+          that.paintCount++;
+        },
+        fail: error => {
+          console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
+          that.triggerEvent('imgErr', {
+            error: error,
+          });
+        },
+      });
+    },
+  },
+});
+
+function setStringPrototype(screenK, scale) {
+  /* eslint-disable no-extend-native */
+  /**
+   * string 到对应的 px
+   * @param {Number} baseSize 当设置了 % 号时,设置的基准值
+   */
+  String.prototype.toPx = function toPx(_, baseSize) {
+    if (this === '0') {
+      return 0;
+    }
+    const REG = /-?[0-9]+(\.[0-9]+)?(rpx|px|%)/;
+
+    const parsePx = origin => {
+      const results = new RegExp(REG).exec(origin);
+      if (!origin || !results) {
+        console.error(`The size: ${origin} is illegal`);
+        return 0;
+      }
+      const unit = results[2];
+      const value = parseFloat(origin);
+
+      let res = 0;
+      if (unit === 'rpx') {
+        res = Math.round(value * (screenK || 0.5) * (scale || 1));
+      } else if (unit === 'px') {
+        res = Math.round(value * (scale || 1));
+      } else if (unit === '%') {
+        res = Math.round((value * baseSize) / 100);
+      }
+      return res;
+    };
+    const formula = /^calc\((.+)\)$/.exec(this);
+    if (formula && formula[1]) {
+      // 进行 calc 计算
+      const afterOne = formula[1].replace(/([^\s\(\+\-\*\/]+)\.(left|right|bottom|top|width|height)/g, word => {
+        const [id, attr] = word.split('.');
+        return penCache.viewRect[id][attr];
+      });
+      const afterTwo = afterOne.replace(new RegExp(REG, 'g'), parsePx);
+      return calc(afterTwo);
+    } else {
+      return parsePx(this);
+    }
+  };
+}

+ 4 - 0
componets/painter/painter.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 21 - 0
componets/painter/painter.wxml

@@ -0,0 +1,21 @@
+<view style='position: relative;{{customStyle}};{{painterStyle}}'>
+  <block wx:if="{{!use2D}}">
+    <canvas canvas-id="photo" style="{{photoStyle}};position: absolute; left: -9999px; top: -9999rpx;" />
+    <block wx:if="{{dancePalette}}">
+      <canvas canvas-id="bottom" style="{{painterStyle}};position: absolute;" />
+      <canvas canvas-id="k-canvas" style="{{painterStyle}};position: absolute;" />
+      <canvas canvas-id="top" style="{{painterStyle}};position: absolute;" />
+      <canvas 
+        canvas-id="front" 
+        style="{{painterStyle}};position: absolute;"
+        bindtouchstart="onTouchStart"
+        bindtouchmove="onTouchMove"
+        bindtouchend="onTouchEnd"
+        bindtouchcancel="onTouchCancel"
+        disable-scroll="{{true}}" />
+      </block>
+  </block>
+  <block wx:if="{{use2D}}">
+    <canvas type="2d" id="photo" style="{{photoStyle}};" />
+  </block>
+</view>

+ 186 - 0
pages/index/getPosterObj.js

@@ -0,0 +1,186 @@
+export function getDrawImg(url, height = 0) {
+    return {
+        obj: [
+            {
+                "type": "image",
+                "url": url, // 确保路径正确
+                "css": {
+                    "top": "0",
+                    "width": 750 + "rpx",
+                    "height": height + 500 + "rpx",
+                    "display": "block",
+                    "borderRadius": "15rpx 15rpx 0 0"
+                }
+            }],
+        height: height + 500
+    }
+}
+export function getCheckInInfo(talk, height = 0) {
+    let mr=48
+    return {
+        obj: [
+            {
+                "type": "image",
+                "url": "/static/painter/bg-1.png",
+                "css": {
+                    "top": height+"rpx",
+                    width: "750rpx",
+                    height: "453rpx",
+                    "borderRadius": "0 0 15rpx 15rpx"
+                }
+            },
+            {
+                "type": "text",
+                "text": "银弘辰悦酒店",
+                "css": {
+                    "top":height+48+"rpx",
+                    "left": mr+"rpx",
+                    fontWeight: "800",
+                    fontSize: "43rpx",
+                    color: "#333333",
+                    lineHeight: "47rpx",
+                    textAlign: "center",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "image",
+                "url": "/static/painter/icon-2.png", // 确保路径正确
+                "css": {
+                    "top": height+124+"rpx",
+                    "left": mr+"rpx",
+                    "width": 34 + "rpx",
+                    "height": 34+ "rpx",
+                    "display": "block",
+                }
+            },
+            {
+                "type": "text",
+                "text": "2025年6月30日",
+                "css": {
+                    "top":height+119+"rpx",
+                    "left": 100+"rpx",
+                    fontWeight: "500",
+                    fontSize: "31rpx",
+                    color: "#666666",
+                    lineHeight: "43rpx",
+                    textAlign: "center",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "image",
+                "url": "/static/painter/icon-1.png", // 确保路径正确
+                "css": {
+                    "top": height+122+"rpx",
+                    "left": 376+"rpx",
+                    "width": 34 + "rpx",
+                    "height": 34+ "rpx",
+                    "display": "block",
+                }
+            },
+            {
+                "type": "text",
+                "text": "2801",
+                "css": {
+                    "top":height+119+"rpx",
+                    "left": 426+"rpx",
+                    fontWeight: "500",
+                    fontSize: "31rpx",
+                    color: "#666666",
+                    lineHeight: "43rpx",
+                    textAlign: "center",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "rect",
+                "css": {
+                    "top":height+119+"rpx",
+                    "left": 347+"rpx",
+                    width: "2rpx",
+                    height: "35rpx",
+                    color: "#D8D8D8",
+                    // shadow: "20rpx 20rpx 8rpx rgba(219,225,231,0.2)",
+                    // borderRadius: '24rpx',
+                }
+            },
+            {
+                "type": "text",
+                "text": "80",
+                "css": {
+                    "top":height+40+"rpx",
+                    "left": 595+"rpx",
+                    fontWeight: "500",
+                    fontSize: "76rpx",
+                    color: "#333333",
+                    lineHeight: "71rpx",
+                    textAlign: "center",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "text",
+                "text": "分",
+                "css": {
+                    "top":height+83+"rpx",
+                    "left": 685+"rpx",
+                    fontWeight: "500",
+                    fontSize: "28rpx",
+                    color: "#666666",
+                    lineHeight: "71rpx",
+                    textAlign: "center",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "rect",
+                "css": {
+                    "top":height+195+"rpx",
+                    "left": mr+"rpx",
+                    width: "655rpx",
+                    height: "0rpx",
+                    borderColor: "#CCCCCC",
+                    borderWidth:"2rpx",
+                    borderStyle: "dashed",
+                }
+            },
+            {
+                "type": "text",
+                "text": "\u3000\u3000"+ talk,
+                "css": {
+                    "top":height+231+"rpx",
+                    "left": mr+"rpx",
+                    "width": "457rpx",
+                    "height": "216rpx",
+                    fontWeight: "500",
+                    fontSize: "28rpx",
+                    color: "#666666",
+                    lineHeight: "38rpx",
+                    fontStyle: "normal",
+                    textTransform: "none"
+                }
+            },
+            {
+                "type": "image",
+                "url": '/static/painter/qr-code.png', // 默认显示“匿名用户”
+                "css": {
+                    "top": height+230+"rpx",
+                    "left": "530rpx",
+                    "width": "171rpx",
+                    "height": "171rpx",
+
+                }
+            },
+        ],
+        height: height + 453
+    }
+}
+
+
+

+ 127 - 2
pages/index/index.js

@@ -1,4 +1,6 @@
 // pages/mine/mine.js
+import {getCheckInInfo, getDrawImg} from "./getPosterObj";
+
 const defaultAvatarUrl = "../../static/images/no-login.png"
 const homeApi_empower = "https://aipush.aidsleep.cn";
 import api from '../../utils/api';
@@ -6,7 +8,10 @@ import api from '../../utils/api';
 
 
 Page({
-
+  imagePath: '',
+  history: [],
+  future: [],
+  isSave: false,
   /**
    * 页面的初始数据
    */
@@ -23,7 +28,126 @@ Page({
     phoneNumber: "",
     latitude: null,
     longitude: null,
-    menuCardList: []
+    menuCardList: [],
+    customActionStyle: {
+      border: {
+        borderColor: '#1A7AF8',
+      },
+      scale: {
+        textIcon: '/palette/switch.png',
+        imageIcon: '/palette/scale.png',
+      },
+      delete: {
+        icon: '/palette/close.png',
+      },
+    },
+    paintPallette: {},
+    template: {},
+    showImg:false
+  },
+  onClickShow() {
+    this.setData({ showImg: true });
+  },
+
+  onClickHide() {
+    this.setData({ showImg: false });
+  },
+
+
+  handleSureDownload() {
+    this.isSave = false;
+    api.painter({
+      params: {score: 69},
+    }).then(data => {
+      if (data.code === 1) {
+
+        var DrawImg = getDrawImg(data?.data?.pic||'/static/painter/hua.png', 0);
+
+        var CheckInInfo = getCheckInInfo(data?.data?.talk|| "亲爱的,哪怕只有一分钟的平静闭眼,也是身体在努力修复的信号。新的一天,新的开始!", DrawImg.height);
+// 定义 palette 对象
+        const palette = {
+          "width": "750rpx",
+          "height": CheckInInfo.height + "rpx",
+          "background": "transparent",
+          "views": [
+            ...DrawImg.obj,
+            ...CheckInInfo.obj,
+          ]
+        };
+// 更新数据到页面
+        this.isSave = true;
+        this.setData({
+          paintPallette: palette
+        });
+      } else {
+      }
+    }).catch(err => {
+    });
+
+  },
+  saveImage() {
+    console.log("saveImage", this.imagePath)
+    if (this.imagePath && typeof this.imagePath === 'string') {
+      this.isSave = false;
+      // 存入系统相册
+      wx.saveImageToPhotosAlbum({
+        filePath: this.imagePath || '',
+        success: res => {
+          this.onClickHide()
+        },
+        fail: res => {
+          this.onClickHide()
+        }
+      })
+    }
+
+  },
+  // 生成海报点击事件
+  onImgOK(e) {
+    this.imagePath = e.detail.path;
+    this.setData({
+      image: this.imagePath,
+    });
+    if (this.isSave) {
+      this.onClickShow()
+    }
+  },
+  touchEnd({detail}) {
+    let needRefresh = detail.index >= 0 && detail.index <= this.data.template.views.length;
+    if (needRefresh) {
+      this.history.push({
+        ...detail,
+      });
+      if (this.data.template.views[detail.index].id === detail.view.id) {
+        this.data.template.views.splice(detail.index, 1);
+      } else {
+        this.data.template.views.splice(detail.index, 0, detail.view);
+      }
+    } else {
+      if (!this.data.template || !this.data.template.views) {
+        return;
+      }
+      for (let view of this.data.template.views) {
+        if (view.id === detail.view.id) {
+          this.history.push({
+            view: {
+              ...detail.view,
+              ...view,
+            },
+          });
+          view.css = detail.view.css;
+          break;
+        }
+      }
+    }
+    this.future.length = 0;
+    const props = {
+      paintPallette: this.data.template,
+    };
+    if (needRefresh) {
+      props.template = this.data.template;
+    }
+    this.setData(props);
   },
   getLocation() {
     const that = this;
@@ -168,6 +292,7 @@ Page({
     const type = this.data.menuList[index].type;
     const url = this.data.menuList[index].url;
     if (!url) {
+      this.handleSureDownload()
       return
     }
     if (type == 'switchTab') {

+ 3 - 1
pages/index/index.json

@@ -1,6 +1,8 @@
 {
   "usingComponents": {
-    "authorized-login-dialog": "../../componets/authorized-login-dialog/authorizedLoginDialog"},
+    "authorized-login-dialog": "../../componets/authorized-login-dialog/authorizedLoginDialog",
+    "painter": "../../componets/painter/painter"
+  },
   "navigationBarRightButton": {
     "hide": true
   },

+ 25 - 1
pages/index/index.wxml

@@ -82,5 +82,29 @@
             </view>
         </view>
     </van-popup>
-
+    <painter
+            customActionStyle="{{customActionStyle}}"
+            palette="{{paintPallette}}"
+            bind:imgOK="onImgOK"
+            bind:touchEnd="touchEnd"
+            widthPixels="1000"
+    />
+    <van-overlay show="{{ showImg }}" bind:click="onClickHide">
+        <view class="wrapper">
+            <view class="block" >
+                <image src="{{image}}"  style="width: 630rpx; height: 100%;"/>
+            </view>
+            <view class="btns-box">
+                <view class="print-btn">
+                    <image class="btn-img"  src="/static/painter/dy-1.png" ></image>
+                    酒店打印
+                </view>
+                <view class="save-btn" bind:tap="saveImage">
+                    <image class="btn-img" src="/static/painter/bc-1.png" ></image>
+                    保存到本地
+                </view>
+            </view>
+        </view>
+    </van-overlay>
+    <!--            widthPixels="1000"-->
 </view>

+ 58 - 0
pages/index/index.wxss

@@ -309,4 +309,62 @@ page {
 .flex-column {
     display: flex;
     flex-direction: column;
+}
+.wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+}
+
+.block {
+    width: 630rpx;
+    height: 800rpx;
+}
+.btns-box {
+    width: 630rpx;
+    padding:80rpx 28rpx 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    box-sizing: border-box;
+}
+.print-btn{
+    width: 272rpx;
+    height: 84rpx;
+    background: #FFFFFF;
+    border-radius: 292rpx 292rpx 292rpx 292rpx;
+    font-weight: normal;
+    font-size: 28rpx;
+    color: #0BC3AA;
+    line-height: 40rpx;
+    text-align: center;
+    font-style: normal;
+    text-transform: none;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.btn-img{
+    width: 28rpx;
+    height: 28rpx;
+    margin-right: 16rpx;
+}
+.save-btn{
+    width: 272rpx;
+    height: 84rpx;
+    background: linear-gradient( 315deg, #0ABCA4 0%, #0BC3AA 100%);
+    border-radius: 292rpx 292rpx 292rpx 292rpx;
+    font-weight: normal;
+    font-size: 28rpx;
+    color: #FFFFFF;
+    line-height: 40rpx;
+    text-align: center;
+    font-style: normal;
+    text-transform: none;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
 }

BIN
static/painter/bc-1.png


BIN
static/painter/bg-1.png


BIN
static/painter/dy-1.png


BIN
static/painter/hua.png


BIN
static/painter/icon-1.png


BIN
static/painter/icon-2.png


BIN
static/painter/qr-code.png


+ 71 - 34
subpagesTwo/seeAlso/seeAico.js

@@ -16,6 +16,7 @@ Page({
         show2Text: '',
         roomInfo: {},
         logsInfo: {},
+        statusFlag:false,
         deviceList: []
     },
     onLoad: function (options) {
@@ -38,68 +39,101 @@ Page({
             show1: false, show2: false
         });
     },
-    onShow() {
+    onClose2() {
         this.setData({
             pageStatus: 0
             , notesInput: ''
             , deviceList: []
             , logsInfo: {}
-            , roomInfo:{}
+            , roomInfo: {}
         });
+        this.setData({
+            show1: false, show2: false
+        });
+    },
+    onShow() {
+
     },
     onShow1() {
         this.setData({
             show1: true
         });
     },
-    onShow2(text=null) {
+    onShow2(text = null) {
         this.setData({
             show2: true,
             show2Text: text
         });
     },
     onStartInspection() {
+        wx.showToast({
+            icon: 'none',
+            title: "开始巡检",
+        });
+        this.setData({pageStatus: 1});
+        this.setData({
+            deviceList: [],
+            logsInfo: {},
+            statusFlag:false,
+            roomInfo: {}
+        })
+
         api.startCheck({
             params: {room_id: this.data.room_id},
             headers: {unionid: wx.getStorageSync("unionid")},
         })
             .then(data => {
                 if (data.code === 1) {
-                    wx.showToast({
-                        icon: 'none',
-                        title: "开始巡检",
-                    });
+                    this.getNucEquips(data.data.logs.id)
+                    setInterval(() => {
+                        this.setData({
+                            statusFlag:true,
+                        })
+                    }, (data.data.ex_time||6)*1000)
+                } else {
                     this.setData({
-                        deviceList:[],
+                        deviceList: [],
                         logsInfo: {},
-                        roomInfo:{}
+                        statusFlag:false,
+                        roomInfo: {}
                     })
-                    this.getNucEquips()
-                    this.setData({pageStatus: 1});
-                } else {
-                    this.onShow2(data.info|| "巡检异常请联系管理员")
+                    this.setData({pageStatus: 0});
+                    if (data[0]==="["){
+                        this.onShow2("当前巡检并未结束,运行结束后再进行操作!")
+                    }else{
+                        this.onShow2(data.info || "巡检异常请联系管理员")
+                    }
+
                 }
             })
             .catch(err => {
-
+                console.log( err)
+                this.setData({
+                    deviceList: [],
+                    statusFlag:false,
+                    logsInfo: {},
+                    roomInfo: {}
+                })
+                this.setData({pageStatus: 0});
                 this.onShow2("巡检异常请联系管理员")
             });
     }
     ,
-    getNucEquips() {
+    getNucEquips(id) {
         api.nucEquips({
-            params: {room_id: this.data.room_id},
+            params: {room_id: this.data.room_id,id: id},
             headers: {unionid: wx.getStorageSync("unionid")}
         }).then(data => {
             if (data.code === 1) {
-                var logs = data.data.logs|| {};
+                var logs = data.data.logs || {};
                 this.setData({
                     deviceList: logs.inspection || [],
-                    logsInfo:logs,
-                    roomInfo:data.data.room||{}
+                    logsInfo: logs,
+                    roomInfo: data.data.room || {}
                 })
-            }else{
-                this.onShow2(data.info|| "获取设备列表失败,请联系管理员!")
+            } else {
+
+                this.onShow2(data.info || "获取设备列表失败,请联系管理员!")
             }
         }).catch(err => {
             this.onShow2("获取设备列表失败,请联系管理员!")
@@ -107,51 +141,54 @@ Page({
     },
     onConfirm() {
         api.confirm({
-            body: {note: this.data.notesInput,id: this.data.logsInfo.id},
+            body: {note: this.data.notesInput, id: this.data.logsInfo.id},
             headers: {unionid: wx.getStorageSync("unionid")}
         }).then(data => {
             if (data.code === 1) {
                 this.setData({
-                     pageStatus: 0
+                    pageStatus: 0
                     , notesInput: ''
                     , deviceList: []
                     , logsInfo: {}
-                    , roomInfo:{}
+                    , roomInfo: {}
                 });
-            }else{
-                this.onShow2(data.info|| "提交失败,请联系管理员!")
+            } else {
+                this.onShow2(data.info || "提交失败,请联系管理员!")
             }
         }).catch(err => {
-            this.onShow2( "提交失败,请联系管理员!")
+            this.onShow2("提交失败,请联系管理员!")
         });
         this.onClose()
     },
     onClosePage() {
-        this.onConfirm()
+       this.onConfirm()
         wx.navigateBack({
             delta: 1
         });
     },
     onSubmit() {
         api.inspect({
-            body: {inspection: this.data.deviceList,id: this.data.logsInfo.id},
+            body: {inspection: this.data.deviceList, id: this.data.logsInfo.id},
             headers: {unionid: wx.getStorageSync("unionid")}
         }).then(data => {
             if (data.code === 1) {
-                let {  count =0,normal=0,abnormal=0, inspection=[]} =data.data||{}
+                let {count = 0, normal = 0, abnormal = 0, inspection = []} = data.data || {}
                 wx.showToast({
                     icon: 'none',
-                    title: data.info|| "提交成功!",
+                    title: data.info || "提交成功!",
                 });
                 this.setData({
                     pageStatus: 2,
                     count,
                     normal,
                     abnormal,
-                    abnormal_d_name: inspection.map(item => item.entity_name).join(',') || '--'
+                    abnormal_d_name: inspection
+                        .filter(item => item.checked === 0)
+                        .map(item => item.entity_name)
+                        .join(',') || '--'
                 });
-            }else{
-                this.onShow2(data.info|| "提交失败,请联系管理员!")
+            } else {
+                this.onShow2(data.info || "提交失败,请联系管理员!")
             }
         }).catch(err => {
             this.onShow2("提交失败,请联系管理员!")

+ 17 - 7
subpagesTwo/seeAlso/seeAico.wxml

@@ -15,21 +15,28 @@
             <view class="start-text">开始巡检</view>
         </view>
         <view class="in-see-aico" wx:if="{{pageStatus===1}}">
-            <view class="in-see-aico-prompt">
+            <view class="in-see-aico-prompt" wx-if="{{!statusFlag}}" >
                 <image src="/subpagesTwo/images/seeAicoDetails/icon-5.png" class="in-see-aico-img"></image>
                 <view class="in-see-aico-prompt-box">
                     <view class="in-see-aico-prompt-title">巡检模式进行中。。。</view>
                     <view class="in-see-aico-prompt-text">巡检模式进行中,设备正在自动启动。请根据设备运行情况填写巡检记录。</view>
                 </view>
             </view>
+            <view class="in-see-aico-prompt flex-vertical-center" wx:else>
+                <image src="/subpagesTwo/images/seeAicoDetails/icon-8.png" class="end-see-aico-img"></image>
+                <view class="in-see-aico-prompt-box">
+                    <view class="end-see-aico-prompt-title">巡检已完成,确认设备情况</view>
+                </view>
+            </view>
             <view class="in-see-aico-list in-see-aico-list-tbm flex-vertical-center flex-space-between"   wx:for="{{ deviceList }}"
                   wx:key="index">
                 <view class="flex-vertical-center">
                     <view class="device-name">
                         {{item.entity_name}}
                     </view>
-                    <image src="{{item.status===0? '/subpagesTwo/images/seeAicoDetails/icon-7.png':'/subpagesTwo/images/seeAicoDetails/icon-6.png'}}" class="device-status-img"></image>
+                    <image src="{{item.checked!==0? '/subpagesTwo/images/seeAicoDetails/icon-7.png':'/subpagesTwo/images/seeAicoDetails/icon-6.png'}}" class="device-status-img"></image>
                 </view>
+
                 <view class="flex-vertical-center" >
                     <view class="in-see-aico-btn flex-vertical-center flex-level-center {{item.checked=== 0 ? 'c-F76666' : 'c-888'}}" style="margin-right: 12rpx"
                     bind:tap="setDeviceStats" data-index="{{ index }}" data-status="{{ 0 }}">
@@ -41,7 +48,10 @@
                     </view>
                 </view>
             </view>
-            <view class="in-see-aico-submit flex-vertical-center flex-level-center" bind:tap="onSubmit" >
+            <view  wx-if="{{!statusFlag}}"   class="in-see-aico-submit flex-vertical-center flex-level-center"  style="background-color: #606972 !important;"  >
+                提交结果
+            </view>
+            <view  wx:else  class="in-see-aico-submit flex-vertical-center flex-level-center" bind:tap="onSubmit"  style="background: linear-gradient(315deg, #0ABCA4 0%, rgba(11, 195, 170, 0.8) 100%);"  >
                 提交结果
             </view>
         </view>
@@ -81,7 +91,7 @@
         </view>
 
     </view>
-    <van-popup  show="{{ show1 }}"  round bind:close="onClose">
+    <van-popup  show="{{ show1 }}"  round bind:close="onConfirm">
         <view class="popup-box-1 flex-column flex-level-center">
             <view class="popup-title-1">重新巡检</view>
             <view class="popup-text-1 flex-level-center">确定要重新巡检该房间吗?</view>
@@ -89,13 +99,13 @@
                 <view bind:tap="onConfirm" class="popup-btn-1 end-see-aico-btn-color-1 flex-vertical-center flex-level-center" style="margin-right: 20rpx">
                     重新巡检
                 </view >
-                <view  bind:tap="onClose"  class="popup-btn-1 end-see-aico-btn-color-2 flex-vertical-center flex-level-center">
+                <view  bind:tap="onConfirm"  class="popup-btn-1 end-see-aico-btn-color-2 flex-vertical-center flex-level-center">
                     关闭
                 </view>
             </view>
         </view>
     </van-popup>
-    <van-popup  show="{{ show2 }}"  round bind:close="onClose">
+    <van-popup  show="{{ show2 }}"  round bind:close="onClose2">
         <view class="popup-box-2 flex-column flex-level-center">
             <view class="flex-level-center">
                 <image src="/subpagesTwo/images/seeAicoDetails/icon-10.png" class="popup-abnormal-img"></image>
@@ -103,7 +113,7 @@
             </view>
             <view class="popup-text-1 flex-level-center">{{show2Text||"巡检未完成,请保持房间网络和电源 畅通后,重新巡检"}}</view>
             <view class="flex-level-center">
-                <view bind:tap="onConfirm" class="popup-btn-2 end-see-aico-btn-color-1 flex-vertical-center flex-level-center">
+                <view bind:tap="onClose2" class="popup-btn-2 end-see-aico-btn-color-1 flex-vertical-center flex-level-center">
                     重新巡检
                 </view >
             </view>

+ 0 - 1
subpagesTwo/seeAlso/seeAico.wxss

@@ -161,7 +161,6 @@ page {
 .in-see-aico-submit {
     width: 630rpx;
     height: 90rpx;
-    background: linear-gradient(315deg, #0ABCA4 0%, rgba(11, 195, 170, 0.8) 100%);
     border-radius: 292rpx 292rpx 292rpx 292rpx;
     font-weight: 400;
     font-size: 32rpx;

+ 7 - 0
utils/api.js

@@ -95,6 +95,13 @@ const apiClient = {
 
         return await apiClient.post('data/api.data/confirm', { body, headers });
     },
+    painter: async ({ params = {}, headers = {} } = {}) => {
+        if (!headers.token) {
+            headers['api-token']  = API_TOKEN;
+        }
+        return await apiClient.get('data/api.painting/paint', { params, headers });
+    },
+
 };
 
 export default apiClient;