wangfumin 1 hete
szülő
commit
9706119137
69 módosított fájl, 6864 hozzáadás és 26 törlés
  1. BIN
      assets/img/edit.png
  2. BIN
      assets/img/icon_correct.png
  3. BIN
      assets/img/icon_delete.png
  4. BIN
      assets/img/icon_rorate.png
  5. BIN
      assets/img/icon_scale.png
  6. BIN
      assets/img/icon_zoomin.png
  7. BIN
      assets/img/icon_zoomout.png
  8. BIN
      assets/img/img_tip.png
  9. 349 0
      miniprogram_npm/eventemitter3/index.js
  10. 1 0
      miniprogram_npm/eventemitter3/index.js.map
  11. 13 0
      miniprogram_npm/widget-ui/index.js
  12. 1 0
      miniprogram_npm/widget-ui/index.js.map
  13. 779 0
      miniprogram_npm/wxml-to-canvas/index.js
  14. 4 0
      miniprogram_npm/wxml-to-canvas/index.json
  15. 2 0
      miniprogram_npm/wxml-to-canvas/index.wxml
  16. 0 0
      miniprogram_npm/wxml-to-canvas/index.wxss
  17. 57 0
      miniprogram_npm/wxml-to-canvas/utils.js
  18. 32 0
      node_modules/.package-lock.json
  19. 21 0
      node_modules/eventemitter3/LICENSE
  20. 94 0
      node_modules/eventemitter3/README.md
  21. 134 0
      node_modules/eventemitter3/index.d.ts
  22. 336 0
      node_modules/eventemitter3/index.js
  23. 56 0
      node_modules/eventemitter3/package.json
  24. 340 0
      node_modules/eventemitter3/umd/eventemitter3.js
  25. 1 0
      node_modules/eventemitter3/umd/eventemitter3.min.js
  26. 1 0
      node_modules/eventemitter3/umd/eventemitter3.min.js.map
  27. 9 0
      node_modules/widget-ui/babel.config.js
  28. 40 0
      node_modules/widget-ui/dist/element.d.ts
  29. 5 0
      node_modules/widget-ui/dist/event.d.ts
  30. 1 0
      node_modules/widget-ui/dist/index.js
  31. 36 0
      node_modules/widget-ui/dist/style.d.ts
  32. 6 0
      node_modules/widget-ui/jest.config.js
  33. 27 0
      node_modules/widget-ui/package.json
  34. 1186 0
      node_modules/widget-ui/src/css-layout.js
  35. 172 0
      node_modules/widget-ui/src/element.ts
  36. 15 0
      node_modules/widget-ui/src/event.ts
  37. 87 0
      node_modules/widget-ui/src/style.ts
  38. 183 0
      node_modules/widget-ui/test/css-layout.test.ts
  39. 47 0
      node_modules/widget-ui/tsconfig.json
  40. 206 0
      node_modules/widget-ui/tslint.json
  41. 25 0
      node_modules/widget-ui/webpack.config.js
  42. 10 0
      node_modules/wxml-to-canvas/.babelrc
  43. 99 0
      node_modules/wxml-to-canvas/.eslintrc.js
  44. 21 0
      node_modules/wxml-to-canvas/LICENSE
  45. 187 0
      node_modules/wxml-to-canvas/README.md
  46. 26 0
      node_modules/wxml-to-canvas/gulpfile.js
  47. 779 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.js
  48. 4 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.json
  49. 2 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.wxml
  50. 0 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.wxss
  51. 57 0
      node_modules/wxml-to-canvas/miniprogram_dist/utils.js
  52. 63 0
      node_modules/wxml-to-canvas/package.json
  53. 225 0
      node_modules/wxml-to-canvas/src/draw.js
  54. 117 0
      node_modules/wxml-to-canvas/src/index.js
  55. 4 0
      node_modules/wxml-to-canvas/src/index.json
  56. 2 0
      node_modules/wxml-to-canvas/src/index.wxml
  57. 0 0
      node_modules/wxml-to-canvas/src/index.wxss
  58. 57 0
      node_modules/wxml-to-canvas/src/utils.js
  59. 81 0
      node_modules/wxml-to-canvas/src/widget.js
  60. 164 0
      node_modules/wxml-to-canvas/src/xml-parser.js
  61. 40 0
      package-lock.json
  62. 14 0
      package.json
  63. 1 1
      pages/index/index.js
  64. 400 2
      pages/work/index.js
  65. 3 0
      pages/work/index.json
  66. 44 1
      pages/work/index.wxml
  67. 163 2
      pages/work/index.wxss
  68. 8 3
      project.config.json
  69. 27 17
      project.private.config.json

BIN
assets/img/edit.png


BIN
assets/img/icon_correct.png


BIN
assets/img/icon_delete.png


BIN
assets/img/icon_rorate.png


BIN
assets/img/icon_scale.png


BIN
assets/img/icon_zoomin.png


BIN
assets/img/icon_zoomout.png


BIN
assets/img/img_tip.png


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 349 - 0
miniprogram_npm/eventemitter3/index.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
miniprogram_npm/eventemitter3/index.js.map


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 13 - 0
miniprogram_npm/widget-ui/index.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
miniprogram_npm/widget-ui/index.js.map


+ 779 - 0
miniprogram_npm/wxml-to-canvas/index.js

@@ -0,0 +1,779 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = factory();
+	else if(typeof define === 'function' && define.amd)
+		define([], factory);
+	else {
+		var a = factory();
+		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
+	}
+})(window, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+/******/
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId]) {
+/******/ 			return installedModules[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = true;
+/******/
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/
+/******/
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+/******/
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// define __esModule on exports
+/******/ 	__webpack_require__.r = function(exports) {
+/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 		}
+/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 	};
+/******/
+/******/ 	// create a fake namespace object
+/******/ 	// mode & 1: value is a module id, require it
+/******/ 	// mode & 2: merge all properties of value into the ns
+/******/ 	// mode & 4: return value when already ns object
+/******/ 	// mode & 8|1: behave like require
+/******/ 	__webpack_require__.t = function(value, mode) {
+/******/ 		if(mode & 1) value = __webpack_require__(value);
+/******/ 		if(mode & 8) return value;
+/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ 		var ns = Object.create(null);
+/******/ 		__webpack_require__.r(ns);
+/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ 		return ns;
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 1);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+const hex = (color) => {
+  let result = null
+
+  if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
+    return color
+    // eslint-disable-next-line no-cond-assign
+  } else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
+    return '#' + result[2].split(',').map((part, index) => {
+      part = part.trim()
+      part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
+      part = part.toString(16)
+      if (part.length === 1) {
+        part = '0' + part
+      }
+      return part
+    }).join('')
+  } else {
+    return '#00000000'
+  }
+}
+
+const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
+  if (index === 0) {
+    return part
+  }
+  return part[0].toUpperCase() + part.slice(1)
+}).join('')
+
+const compareVersion = (v1, v2) => {
+  v1 = v1.split('.')
+  v2 = v2.split('.')
+  const len = Math.max(v1.length, v2.length)
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i], 10)
+    const num2 = parseInt(v2[i], 10)
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+module.exports = {
+  hex,
+  splitLineToCamelCase,
+  compareVersion
+}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+const xmlParse = __webpack_require__(2)
+const {Widget} = __webpack_require__(3)
+const {Draw} = __webpack_require__(5)
+const {compareVersion} = __webpack_require__(0)
+
+const canvasId = 'weui-canvas'
+
+Component({
+  properties: {
+    width: {
+      type: Number,
+      value: 400
+    },
+    height: {
+      type: Number,
+      value: 300
+    }
+  },
+  data: {
+    use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
+  },
+  lifetimes: {
+    attached() {
+      const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
+      const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
+      this.dpr = dpr
+      this.setData({use2dCanvas}, () => {
+        if (use2dCanvas) {
+          const query = this.createSelectorQuery()
+          query.select(`#${canvasId}`)
+            .fields({node: true, size: true})
+            .exec(res => {
+              const canvas = res[0].node
+              const ctx = canvas.getContext('2d')
+              canvas.width = res[0].width * dpr
+              canvas.height = res[0].height * dpr
+              ctx.scale(dpr, dpr)
+              this.ctx = ctx
+              this.canvas = canvas
+            })
+        } else {
+          this.ctx = wx.createCanvasContext(canvasId, this)
+        }
+      })
+    }
+  },
+  methods: {
+    async renderToCanvas(args) {
+      const {wxml, style} = args
+      const ctx = this.ctx
+      const canvas = this.canvas
+      const use2dCanvas = this.data.use2dCanvas
+
+      if (use2dCanvas && !canvas) {
+        return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
+      }
+
+      ctx.clearRect(0, 0, this.data.width, this.data.height)
+      const {root: xom} = xmlParse(wxml)
+
+      const widget = new Widget(xom, style)
+      const container = widget.init()
+      this.boundary = {
+        top: container.layoutBox.top,
+        left: container.layoutBox.left,
+        width: container.computedStyle.width,
+        height: container.computedStyle.height,
+      }
+      const draw = new Draw(ctx, canvas, use2dCanvas)
+      await draw.drawNode(container)
+
+      if (!use2dCanvas) {
+        await this.canvasDraw(ctx)
+      }
+      return Promise.resolve(container)
+    },
+
+    canvasDraw(ctx, reserve) {
+      return new Promise(resolve => {
+        ctx.draw(reserve, () => {
+          resolve()
+        })
+      })
+    },
+
+    canvasToTempFilePath(args = {}) {
+      const use2dCanvas = this.data.use2dCanvas
+
+      return new Promise((resolve, reject) => {
+        const {
+          top, left, width, height
+        } = this.boundary
+
+        const copyArgs = {
+          x: left,
+          y: top,
+          width,
+          height,
+          destWidth: width * this.dpr,
+          destHeight: height * this.dpr,
+          canvasId,
+          fileType: args.fileType || 'png',
+          quality: args.quality || 1,
+          success: resolve,
+          fail: reject
+        }
+
+        if (use2dCanvas) {
+          delete copyArgs.canvasId
+          copyArgs.canvas = this.canvas
+        }
+        wx.canvasToTempFilePath(copyArgs, this)
+      })
+    }
+  }
+})
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+
+/**
+ * Module dependencies.
+ */
+
+
+/**
+ * Expose `parse`.
+ */
+
+
+/**
+ * Parse the given string of `xml`.
+ *
+ * @param {String} xml
+ * @return {Object}
+ * @api public
+ */
+
+function parse(xml) {
+  xml = xml.trim()
+
+  // strip comments
+  xml = xml.replace(/<!--[\s\S]*?-->/g, '')
+
+  return document()
+
+  /**
+   * XML document.
+   */
+
+  function document() {
+    return {
+      declaration: declaration(),
+      root: tag()
+    }
+  }
+
+  /**
+   * Declaration.
+   */
+
+  function declaration() {
+    const m = match(/^<\?xml\s*/)
+    if (!m) return
+
+    // tag
+    const node = {
+      attributes: {}
+    }
+
+    // attributes
+    while (!(eos() || is('?>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    match(/\?>\s*/)
+
+    return node
+  }
+
+  /**
+   * Tag.
+   */
+
+  function tag() {
+    const m = match(/^<([\w-:.]+)\s*/)
+    if (!m) return
+
+    // name
+    const node = {
+      name: m[1],
+      attributes: {},
+      children: []
+    }
+
+    // attributes
+    while (!(eos() || is('>') || is('?>') || is('/>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    // self closing tag
+    if (match(/^\s*\/>\s*/)) {
+      return node
+    }
+
+    match(/\??>\s*/)
+
+    // content
+    node.content = content()
+
+    // children
+    let child
+    while (child = tag()) {
+      node.children.push(child)
+    }
+
+    // closing
+    match(/^<\/[\w-:.]+>\s*/)
+
+    return node
+  }
+
+  /**
+   * Text content.
+   */
+
+  function content() {
+    const m = match(/^([^<]*)/)
+    if (m) return m[1]
+    return ''
+  }
+
+  /**
+   * Attribute.
+   */
+
+  function attribute() {
+    const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
+    if (!m) return
+    return {name: m[1], value: strip(m[2])}
+  }
+
+  /**
+   * Strip quotes from `val`.
+   */
+
+  function strip(val) {
+    return val.replace(/^['"]|['"]$/g, '')
+  }
+
+  /**
+   * Match `re` and advance the string.
+   */
+
+  function match(re) {
+    const m = xml.match(re)
+    if (!m) return
+    xml = xml.slice(m[0].length)
+    return m
+  }
+
+  /**
+   * End-of-source.
+   */
+
+  function eos() {
+    return xml.length == 0
+  }
+
+  /**
+   * Check for `prefix`.
+   */
+
+  function is(prefix) {
+    return xml.indexOf(prefix) == 0
+  }
+}
+
+module.exports = parse
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+const Block = __webpack_require__(4)
+const {splitLineToCamelCase} = __webpack_require__(0)
+
+class Element extends Block {
+  constructor(prop) {
+    super(prop.style)
+    this.name = prop.name
+    this.attributes = prop.attributes
+  }
+}
+
+
+class Widget {
+  constructor(xom, style) {
+    this.xom = xom
+    this.style = style
+
+    this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
+  }
+
+  init() {
+    this.container = this.create(this.xom)
+    this.container.layout()
+
+    this.inheritStyle(this.container)
+    return this.container
+  }
+
+  // 继承父节点的样式
+  inheritStyle(node) {
+    const parent = node.parent || null
+    const children = node.children || {}
+    const computedStyle = node.computedStyle
+
+    if (parent) {
+      this.inheritProps.forEach(prop => {
+        computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
+      })
+    }
+
+    Object.values(children).forEach(child => {
+      this.inheritStyle(child)
+    })
+  }
+
+  create(node) {
+    let classNames = (node.attributes.class || '').split(' ')
+    classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
+    const style = {}
+    classNames.forEach(item => {
+      Object.assign(style, this.style[item] || {})
+    })
+
+    const args = {name: node.name, style}
+
+    const attrs = Object.keys(node.attributes)
+    const attributes = {}
+    for (const attr of attrs) {
+      const value = node.attributes[attr]
+      const CamelAttr = splitLineToCamelCase(attr)
+
+      if (value === '' || value === 'true') {
+        attributes[CamelAttr] = true
+      } else if (value === 'false') {
+        attributes[CamelAttr] = false
+      } else {
+        attributes[CamelAttr] = value
+      }
+    }
+    attributes.text = node.content
+    args.attributes = attributes
+    const element = new Element(args)
+    node.children.forEach(childNode => {
+      const childElement = this.create(childNode)
+      element.add(childElement)
+    })
+    return element
+  }
+}
+
+module.exports = {Widget}
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+module.exports = require("widget-ui");
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
+class Draw {
+  constructor(context, canvas, use2dCanvas = false) {
+    this.ctx = context
+    this.canvas = canvas || null
+    this.use2dCanvas = use2dCanvas
+  }
+
+  roundRect(x, y, w, h, r, fill = true, stroke = false) {
+    if (r < 0) return
+    const ctx = this.ctx
+
+    ctx.beginPath()
+    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
+    ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
+    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
+    ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
+    ctx.lineTo(x, y + r)
+    if (stroke) ctx.stroke()
+    if (fill) ctx.fill()
+  }
+
+  drawView(box, style) {
+    const ctx = this.ctx
+    const {
+      left: x, top: y, width: w, height: h
+    } = box
+    const {
+      borderRadius = 0,
+      borderWidth = 0,
+      borderColor,
+      color = '#000',
+      backgroundColor = 'transparent',
+    } = style
+    ctx.save()
+    // 外环
+    if (borderWidth > 0) {
+      ctx.fillStyle = borderColor || color
+      this.roundRect(x, y, w, h, borderRadius)
+    }
+
+    // 内环
+    ctx.fillStyle = backgroundColor
+    const innerWidth = w - 2 * borderWidth
+    const innerHeight = h - 2 * borderWidth
+    const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
+    this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
+    ctx.restore()
+  }
+
+  async drawImage(img, box, style) {
+    await new Promise((resolve, reject) => {
+      const ctx = this.ctx
+      const canvas = this.canvas
+
+      const {
+        borderRadius = 0
+      } = style
+      const {
+        left: x, top: y, width: w, height: h
+      } = box
+      ctx.save()
+      this.roundRect(x, y, w, h, borderRadius, false, false)
+      ctx.clip()
+
+      const _drawImage = (img) => {
+        if (this.use2dCanvas) {
+          const Image = canvas.createImage()
+          Image.onload = () => {
+            ctx.drawImage(Image, x, y, w, h)
+            ctx.restore()
+            resolve()
+          }
+          Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
+          Image.src = img
+        } else {
+          ctx.drawImage(img, x, y, w, h)
+          ctx.restore()
+          resolve()
+        }
+      }
+
+      const isTempFile = /^wxfile:\/\//.test(img)
+      const isNetworkFile = /^https?:\/\//.test(img)
+
+      if (isTempFile) {
+        _drawImage(img)
+      } else if (isNetworkFile) {
+        wx.downloadFile({
+          url: img,
+          success(res) {
+            if (res.statusCode === 200) {
+              _drawImage(res.tempFilePath)
+            } else {
+              reject(new Error(`downloadFile:fail ${img}`))
+            }
+          },
+          fail() {
+            reject(new Error(`downloadFile:fail ${img}`))
+          }
+        })
+      } else {
+        reject(new Error(`image format error: ${img}`))
+      }
+    })
+  }
+
+  // eslint-disable-next-line complexity
+  drawText(text, box, style) {
+    const ctx = this.ctx
+    let {
+      left: x, top: y, width: w, height: h
+    } = box
+    let {
+      color = '#000',
+      lineHeight = '1.4em',
+      fontSize = 14,
+      textAlign = 'left',
+      verticalAlign = 'top',
+      backgroundColor = 'transparent'
+    } = style
+
+    if (typeof lineHeight === 'string') { // 2em
+      lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
+    }
+    if (!text || (lineHeight > h)) return
+
+    ctx.save()
+    ctx.textBaseline = 'top'
+    ctx.font = `${fontSize}px sans-serif`
+    ctx.textAlign = textAlign
+
+    // 背景色
+    ctx.fillStyle = backgroundColor
+    this.roundRect(x, y, w, h, 0)
+
+    // 文字颜色
+    ctx.fillStyle = color
+
+    // 水平布局
+    switch (textAlign) {
+      case 'left':
+        break
+      case 'center':
+        x += 0.5 * w
+        break
+      case 'right':
+        x += w
+        break
+      default: break
+    }
+
+    const textWidth = ctx.measureText(text).width
+    const actualHeight = Math.ceil(textWidth / w) * lineHeight
+    let paddingTop = Math.ceil((h - actualHeight) / 2)
+    if (paddingTop < 0) paddingTop = 0
+
+    // 垂直布局
+    switch (verticalAlign) {
+      case 'top':
+        break
+      case 'middle':
+        y += paddingTop
+        break
+      case 'bottom':
+        y += 2 * paddingTop
+        break
+      default: break
+    }
+
+    const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
+
+    // 不超过一行
+    if (textWidth <= w) {
+      ctx.fillText(text, x, y + inlinePaddingTop)
+      return
+    }
+
+    // 多行文本
+    const chars = text.split('')
+    const _y = y
+
+    // 逐行绘制
+    let line = ''
+    for (const ch of chars) {
+      const testLine = line + ch
+      const testWidth = ctx.measureText(testLine).width
+
+      if (testWidth > w) {
+        ctx.fillText(line, x, y + inlinePaddingTop)
+        y += lineHeight
+        line = ch
+        if ((y + lineHeight) > (_y + h)) break
+      } else {
+        line = testLine
+      }
+    }
+
+    // 避免溢出
+    if ((y + lineHeight) <= (_y + h)) {
+      ctx.fillText(line, x, y + inlinePaddingTop)
+    }
+    ctx.restore()
+  }
+
+  async drawNode(element) {
+    const {layoutBox, computedStyle, name} = element
+    const {src, text} = element.attributes
+    if (name === 'view') {
+      this.drawView(layoutBox, computedStyle)
+    } else if (name === 'image') {
+      await this.drawImage(src, layoutBox, computedStyle)
+    } else if (name === 'text') {
+      this.drawText(text, layoutBox, computedStyle)
+    }
+    const childs = Object.values(element.children)
+    for (const child of childs) {
+      await this.drawNode(child)
+    }
+  }
+}
+
+
+module.exports = {
+  Draw
+}
+
+
+/***/ })
+/******/ ]);
+});

+ 4 - 0
miniprogram_npm/wxml-to-canvas/index.json

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

+ 2 - 0
miniprogram_npm/wxml-to-canvas/index.wxml

@@ -0,0 +1,2 @@
+<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
+<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

+ 0 - 0
miniprogram_npm/wxml-to-canvas/index.wxss


+ 57 - 0
miniprogram_npm/wxml-to-canvas/utils.js

@@ -0,0 +1,57 @@
+const hex = (color) => {
+  let result = null
+
+  if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
+    return color
+    // eslint-disable-next-line no-cond-assign
+  } else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
+    return '#' + result[2].split(',').map((part, index) => {
+      part = part.trim()
+      part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
+      part = part.toString(16)
+      if (part.length === 1) {
+        part = '0' + part
+      }
+      return part
+    }).join('')
+  } else {
+    return '#00000000'
+  }
+}
+
+const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
+  if (index === 0) {
+    return part
+  }
+  return part[0].toUpperCase() + part.slice(1)
+}).join('')
+
+const compareVersion = (v1, v2) => {
+  v1 = v1.split('.')
+  v2 = v2.split('.')
+  const len = Math.max(v1.length, v2.length)
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i], 10)
+    const num2 = parseInt(v2[i], 10)
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+module.exports = {
+  hex,
+  splitLineToCamelCase,
+  compareVersion
+}

+ 32 - 0
node_modules/.package-lock.json

@@ -0,0 +1,32 @@
+{
+  "name": "swk",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
+    "node_modules/widget-ui": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz",
+      "integrity": "sha512-gDXosr5mflJdMA1weU1A47aTsTFfMJhfA4EKgO5XFebY3eVklf80KD4GODfrjo8J2WQ+9YjL1Rd9UUmKIzhShw==",
+      "license": "ISC",
+      "dependencies": {
+        "eventemitter3": "^4.0.0"
+      }
+    },
+    "node_modules/wxml-to-canvas": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/wxml-to-canvas/-/wxml-to-canvas-1.1.1.tgz",
+      "integrity": "sha512-3mDjHzujY/UgdCOXij/MnmwJYerVjwkyQHMBFBE8zh89DK7h7UTzoydWFqEBjIC0rfZM+AXl5kDh9hUcsNpSmg==",
+      "license": "MIT",
+      "dependencies": {
+        "widget-ui": "^1.0.2"
+      }
+    }
+  }
+}

+ 21 - 0
node_modules/eventemitter3/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Arnout Kazemier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 94 - 0
node_modules/eventemitter3/README.md


+ 134 - 0
node_modules/eventemitter3/index.d.ts

@@ -0,0 +1,134 @@
+/**
+ * Minimal `EventEmitter` interface that is molded against the Node.js
+ * `EventEmitter` interface.
+ */
+declare class EventEmitter<
+  EventTypes extends EventEmitter.ValidEventTypes = string | symbol,
+  Context extends any = any
+> {
+  static prefixed: string | boolean;
+
+  /**
+   * Return an array listing the events for which the emitter has registered
+   * listeners.
+   */
+  eventNames(): Array<EventEmitter.EventNames<EventTypes>>;
+
+  /**
+   * Return the listeners registered for a given event.
+   */
+  listeners<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T
+  ): Array<EventEmitter.EventListener<EventTypes, T>>;
+
+  /**
+   * Return the number of listeners listening to a given event.
+   */
+  listenerCount(event: EventEmitter.EventNames<EventTypes>): number;
+
+  /**
+   * Calls each of the listeners registered for a given event.
+   */
+  emit<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    ...args: EventEmitter.EventArgs<EventTypes, T>
+  ): boolean;
+
+  /**
+   * Add a listener for a given event.
+   */
+  on<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+  ): this;
+  addListener<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+  ): this;
+
+  /**
+   * Add a one-time listener for a given event.
+   */
+  once<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+  ): this;
+
+  /**
+   * Remove the listeners of a given event.
+   */
+  removeListener<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn?: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context,
+    once?: boolean
+  ): this;
+  off<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn?: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context,
+    once?: boolean
+  ): this;
+
+  /**
+   * Remove all listeners, or those of the specified event.
+   */
+  removeAllListeners(event?: EventEmitter.EventNames<EventTypes>): this;
+}
+
+declare namespace EventEmitter {
+  export interface ListenerFn<Args extends any[] = any[]> {
+    (...args: Args): void;
+  }
+
+  export interface EventEmitterStatic {
+    new <
+      EventTypes extends ValidEventTypes = string | symbol,
+      Context = any
+    >(): EventEmitter<EventTypes, Context>;
+  }
+
+  /**
+   * `object` should be in either of the following forms:
+   * ```
+   * interface EventTypes {
+   *   'event-with-parameters': any[]
+   *   'event-with-example-handler': (...args: any[]) => void
+   * }
+   * ```
+   */
+  export type ValidEventTypes = string | symbol | object;
+
+  export type EventNames<T extends ValidEventTypes> = T extends string | symbol
+    ? T
+    : keyof T;
+
+  export type ArgumentMap<T extends object> = {
+    [K in keyof T]: T[K] extends (...args: any[]) => void
+      ? Parameters<T[K]>
+      : T[K] extends any[]
+      ? T[K]
+      : any[];
+  };
+
+  export type EventListener<
+    T extends ValidEventTypes,
+    K extends EventNames<T>
+  > = T extends string | symbol
+    ? (...args: any[]) => void
+    : (
+        ...args: ArgumentMap<Exclude<T, string | symbol>>[Extract<K, keyof T>]
+      ) => void;
+
+  export type EventArgs<
+    T extends ValidEventTypes,
+    K extends EventNames<T>
+  > = Parameters<EventListener<T, K>>;
+
+  export const EventEmitter: EventEmitterStatic;
+}
+
+export = EventEmitter;

+ 336 - 0
node_modules/eventemitter3/index.js

@@ -0,0 +1,336 @@
+'use strict';
+
+var has = Object.prototype.hasOwnProperty
+  , prefix = '~';
+
+/**
+ * Constructor to create a storage for our `EE` objects.
+ * An `Events` instance is a plain object whose properties are event names.
+ *
+ * @constructor
+ * @private
+ */
+function Events() {}
+
+//
+// We try to not inherit from `Object.prototype`. In some engines creating an
+// instance in this way is faster than calling `Object.create(null)` directly.
+// If `Object.create(null)` is not supported we prefix the event names with a
+// character to make sure that the built-in object properties are not
+// overridden or used as an attack vector.
+//
+if (Object.create) {
+  Events.prototype = Object.create(null);
+
+  //
+  // This hack is needed because the `__proto__` property is still inherited in
+  // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
+  //
+  if (!new Events().__proto__) prefix = false;
+}
+
+/**
+ * Representation of a single event listener.
+ *
+ * @param {Function} fn The listener function.
+ * @param {*} context The context to invoke the listener with.
+ * @param {Boolean} [once=false] Specify if the listener is a one-time listener.
+ * @constructor
+ * @private
+ */
+function EE(fn, context, once) {
+  this.fn = fn;
+  this.context = context;
+  this.once = once || false;
+}
+
+/**
+ * Add a listener for a given event.
+ *
+ * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
+ * @param {(String|Symbol)} event The event name.
+ * @param {Function} fn The listener function.
+ * @param {*} context The context to invoke the listener with.
+ * @param {Boolean} once Specify if the listener is a one-time listener.
+ * @returns {EventEmitter}
+ * @private
+ */
+function addListener(emitter, event, fn, context, once) {
+  if (typeof fn !== 'function') {
+    throw new TypeError('The listener must be a function');
+  }
+
+  var listener = new EE(fn, context || emitter, once)
+    , evt = prefix ? prefix + event : event;
+
+  if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
+  else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
+  else emitter._events[evt] = [emitter._events[evt], listener];
+
+  return emitter;
+}
+
+/**
+ * Clear event by name.
+ *
+ * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
+ * @param {(String|Symbol)} evt The Event name.
+ * @private
+ */
+function clearEvent(emitter, evt) {
+  if (--emitter._eventsCount === 0) emitter._events = new Events();
+  else delete emitter._events[evt];
+}
+
+/**
+ * Minimal `EventEmitter` interface that is molded against the Node.js
+ * `EventEmitter` interface.
+ *
+ * @constructor
+ * @public
+ */
+function EventEmitter() {
+  this._events = new Events();
+  this._eventsCount = 0;
+}
+
+/**
+ * Return an array listing the events for which the emitter has registered
+ * listeners.
+ *
+ * @returns {Array}
+ * @public
+ */
+EventEmitter.prototype.eventNames = function eventNames() {
+  var names = []
+    , events
+    , name;
+
+  if (this._eventsCount === 0) return names;
+
+  for (name in (events = this._events)) {
+    if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
+  }
+
+  if (Object.getOwnPropertySymbols) {
+    return names.concat(Object.getOwnPropertySymbols(events));
+  }
+
+  return names;
+};
+
+/**
+ * Return the listeners registered for a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @returns {Array} The registered listeners.
+ * @public
+ */
+EventEmitter.prototype.listeners = function listeners(event) {
+  var evt = prefix ? prefix + event : event
+    , handlers = this._events[evt];
+
+  if (!handlers) return [];
+  if (handlers.fn) return [handlers.fn];
+
+  for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
+    ee[i] = handlers[i].fn;
+  }
+
+  return ee;
+};
+
+/**
+ * Return the number of listeners listening to a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @returns {Number} The number of listeners.
+ * @public
+ */
+EventEmitter.prototype.listenerCount = function listenerCount(event) {
+  var evt = prefix ? prefix + event : event
+    , listeners = this._events[evt];
+
+  if (!listeners) return 0;
+  if (listeners.fn) return 1;
+  return listeners.length;
+};
+
+/**
+ * Calls each of the listeners registered for a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @returns {Boolean} `true` if the event had listeners, else `false`.
+ * @public
+ */
+EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
+  var evt = prefix ? prefix + event : event;
+
+  if (!this._events[evt]) return false;
+
+  var listeners = this._events[evt]
+    , len = arguments.length
+    , args
+    , i;
+
+  if (listeners.fn) {
+    if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
+
+    switch (len) {
+      case 1: return listeners.fn.call(listeners.context), true;
+      case 2: return listeners.fn.call(listeners.context, a1), true;
+      case 3: return listeners.fn.call(listeners.context, a1, a2), true;
+      case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
+      case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
+      case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
+    }
+
+    for (i = 1, args = new Array(len -1); i < len; i++) {
+      args[i - 1] = arguments[i];
+    }
+
+    listeners.fn.apply(listeners.context, args);
+  } else {
+    var length = listeners.length
+      , j;
+
+    for (i = 0; i < length; i++) {
+      if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
+
+      switch (len) {
+        case 1: listeners[i].fn.call(listeners[i].context); break;
+        case 2: listeners[i].fn.call(listeners[i].context, a1); break;
+        case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
+        case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
+        default:
+          if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
+            args[j - 1] = arguments[j];
+          }
+
+          listeners[i].fn.apply(listeners[i].context, args);
+      }
+    }
+  }
+
+  return true;
+};
+
+/**
+ * Add a listener for a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @param {Function} fn The listener function.
+ * @param {*} [context=this] The context to invoke the listener with.
+ * @returns {EventEmitter} `this`.
+ * @public
+ */
+EventEmitter.prototype.on = function on(event, fn, context) {
+  return addListener(this, event, fn, context, false);
+};
+
+/**
+ * Add a one-time listener for a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @param {Function} fn The listener function.
+ * @param {*} [context=this] The context to invoke the listener with.
+ * @returns {EventEmitter} `this`.
+ * @public
+ */
+EventEmitter.prototype.once = function once(event, fn, context) {
+  return addListener(this, event, fn, context, true);
+};
+
+/**
+ * Remove the listeners of a given event.
+ *
+ * @param {(String|Symbol)} event The event name.
+ * @param {Function} fn Only remove the listeners that match this function.
+ * @param {*} context Only remove the listeners that have this context.
+ * @param {Boolean} once Only remove one-time listeners.
+ * @returns {EventEmitter} `this`.
+ * @public
+ */
+EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
+  var evt = prefix ? prefix + event : event;
+
+  if (!this._events[evt]) return this;
+  if (!fn) {
+    clearEvent(this, evt);
+    return this;
+  }
+
+  var listeners = this._events[evt];
+
+  if (listeners.fn) {
+    if (
+      listeners.fn === fn &&
+      (!once || listeners.once) &&
+      (!context || listeners.context === context)
+    ) {
+      clearEvent(this, evt);
+    }
+  } else {
+    for (var i = 0, events = [], length = listeners.length; i < length; i++) {
+      if (
+        listeners[i].fn !== fn ||
+        (once && !listeners[i].once) ||
+        (context && listeners[i].context !== context)
+      ) {
+        events.push(listeners[i]);
+      }
+    }
+
+    //
+    // Reset the array, or remove it completely if we have no more listeners.
+    //
+    if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
+    else clearEvent(this, evt);
+  }
+
+  return this;
+};
+
+/**
+ * Remove all listeners, or those of the specified event.
+ *
+ * @param {(String|Symbol)} [event] The event name.
+ * @returns {EventEmitter} `this`.
+ * @public
+ */
+EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
+  var evt;
+
+  if (event) {
+    evt = prefix ? prefix + event : event;
+    if (this._events[evt]) clearEvent(this, evt);
+  } else {
+    this._events = new Events();
+    this._eventsCount = 0;
+  }
+
+  return this;
+};
+
+//
+// Alias methods names because people roll like that.
+//
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+EventEmitter.prototype.addListener = EventEmitter.prototype.on;
+
+//
+// Expose the prefix.
+//
+EventEmitter.prefixed = prefix;
+
+//
+// Allow `EventEmitter` to be imported as module namespace.
+//
+EventEmitter.EventEmitter = EventEmitter;
+
+//
+// Expose the module.
+//
+if ('undefined' !== typeof module) {
+  module.exports = EventEmitter;
+}

+ 56 - 0
node_modules/eventemitter3/package.json

@@ -0,0 +1,56 @@
+{
+  "name": "eventemitter3",
+  "version": "4.0.7",
+  "description": "EventEmitter3 focuses on performance while maintaining a Node.js AND browser compatible interface.",
+  "main": "index.js",
+  "typings": "index.d.ts",
+  "scripts": {
+    "browserify": "rm -rf umd && mkdir umd && browserify index.js -s EventEmitter3 -o umd/eventemitter3.js",
+    "minify": "uglifyjs umd/eventemitter3.js --source-map -cm -o umd/eventemitter3.min.js",
+    "benchmark": "find benchmarks/run -name '*.js' -exec benchmarks/start.sh {} \\;",
+    "test": "nyc --reporter=html --reporter=text mocha test/test.js",
+    "prepublishOnly": "npm run browserify && npm run minify",
+    "test-browser": "node test/browser.js"
+  },
+  "files": [
+    "index.js",
+    "index.d.ts",
+    "umd"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/primus/eventemitter3.git"
+  },
+  "keywords": [
+    "EventEmitter",
+    "EventEmitter2",
+    "EventEmitter3",
+    "Events",
+    "addEventListener",
+    "addListener",
+    "emit",
+    "emits",
+    "emitter",
+    "event",
+    "once",
+    "pub/sub",
+    "publish",
+    "reactor",
+    "subscribe"
+  ],
+  "author": "Arnout Kazemier",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/primus/eventemitter3/issues"
+  },
+  "devDependencies": {
+    "assume": "^2.2.0",
+    "browserify": "^16.5.0",
+    "mocha": "^8.0.1",
+    "nyc": "^15.1.0",
+    "pre-commit": "^1.2.0",
+    "sauce-browsers": "^2.0.0",
+    "sauce-test": "^1.3.3",
+    "uglify-js": "^3.9.0"
+  }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 340 - 0
node_modules/eventemitter3/umd/eventemitter3.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
node_modules/eventemitter3/umd/eventemitter3.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
node_modules/eventemitter3/umd/eventemitter3.min.js.map


+ 9 - 0
node_modules/widget-ui/babel.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  presets: [
+    ["@babel/preset-env", {
+      targets: {
+        node: "current"
+      }
+    }]
+  ]
+};

+ 40 - 0
node_modules/widget-ui/dist/element.d.ts

@@ -0,0 +1,40 @@
+declare type LayoutData = {
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+};
+declare type LayoutNode = {
+    id: number;
+    style: Object;
+    children: LayoutNode[];
+    layout?: LayoutData;
+};
+declare class Element {
+    static uuid(): number;
+    parent: Element | null;
+    id: number;
+    style: {
+        [key: string]: any;
+    };
+    computedStyle: {
+        [key: string]: any;
+    };
+    lastComputedStyle: {
+        [key: string]: any;
+    };
+    children: {
+        [key: string]: Element;
+    };
+    layoutBox: LayoutData;
+    constructor(style?: {
+        [key: string]: any;
+    });
+    getAbsolutePosition(element: Element): any;
+    add(element: Element): void;
+    remove(element?: Element): void;
+    getNodeTree(): LayoutNode;
+    applyLayout(layoutNode: LayoutNode): void;
+    layout(): void;
+}
+export default Element;

+ 5 - 0
node_modules/widget-ui/dist/event.d.ts

@@ -0,0 +1,5 @@
+export default class EventEmitter {
+    emit(event: string, data?: any): void;
+    on(event: string, callback: any): void;
+    off(event: string, callback: any): void;
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
node_modules/widget-ui/dist/index.js


+ 36 - 0
node_modules/widget-ui/dist/style.d.ts

@@ -0,0 +1,36 @@
+declare const textStyles: string[];
+declare const scalableStyles: string[];
+declare const layoutAffectedStyles: string[];
+declare const getDefaultStyle: () => {
+    left: undefined;
+    top: undefined;
+    right: undefined;
+    bottom: undefined;
+    width: undefined;
+    height: undefined;
+    maxWidth: undefined;
+    maxHeight: undefined;
+    minWidth: undefined;
+    minHeight: undefined;
+    margin: undefined;
+    marginLeft: undefined;
+    marginRight: undefined;
+    marginTop: undefined;
+    marginBottom: undefined;
+    padding: undefined;
+    paddingLeft: undefined;
+    paddingRight: undefined;
+    paddingTop: undefined;
+    paddingBottom: undefined;
+    borderWidth: undefined;
+    flexDirection: undefined;
+    justifyContent: undefined;
+    alignItems: undefined;
+    alignSelf: undefined;
+    flex: undefined;
+    flexWrap: undefined;
+    position: undefined;
+    hidden: boolean;
+    scale: number;
+};
+export { getDefaultStyle, scalableStyles, textStyles, layoutAffectedStyles };

+ 6 - 0
node_modules/widget-ui/jest.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  transform: {
+    "^.+\\.js$": "babel-jest",
+    "^.+\\.ts$": "ts-jest"
+  }
+};

+ 27 - 0
node_modules/widget-ui/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "widget-ui",
+  "version": "1.0.2",
+  "description": "",
+  "main": "dist/index.js",
+  "scripts": {
+    "test": "jest",
+    "build": "webpack"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "eventemitter3": "^4.0.0"
+  },
+  "devDependencies": {
+    "@babel/preset-env": "^7.6.3",
+    "@babel/preset-typescript": "^7.6.0",
+    "@types/jest": "^24.0.18",
+    "babel-jest": "^24.9.0",
+    "jest": "^24.9.0",
+    "ts-jest": "^24.1.0",
+    "ts-loader": "^6.2.0",
+    "typescript": "^3.6.4",
+    "webpack": "^4.41.1",
+    "webpack-cli": "^3.3.9"
+  }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1186 - 0
node_modules/widget-ui/src/css-layout.js


+ 172 - 0
node_modules/widget-ui/src/element.ts

@@ -0,0 +1,172 @@
+
+import computeLayout from "./css-layout";
+import { getDefaultStyle, scalableStyles, layoutAffectedStyles } from "./style";
+
+type LayoutData = {
+  left: number,
+  top: number,
+  width: number,
+  height: number
+};
+
+type LayoutNode = {
+  id: number,
+  style: Object,
+  children: LayoutNode[],
+  layout?: LayoutData
+};
+
+let uuid = 0;
+
+class Element {
+  public static uuid(): number {
+    return uuid++;
+  }
+
+  public parent: Element | null = null;
+  public id: number = Element.uuid();
+  public style: { [key: string]: any } = {};
+  public computedStyle: { [key: string]: any } = {};
+  public lastComputedStyle: { [key: string]: any } = {};
+  public children: { [key: string]: Element } = {};
+  public layoutBox: LayoutData = { left: 0, top: 0, width: 0, height: 0 };
+
+  constructor(style: { [key: string]: any } = {}) {
+    // 拷贝一份,防止被外部逻辑修改
+    style = Object.assign(getDefaultStyle(), style);
+    this.computedStyle = Object.assign(getDefaultStyle(), style);
+    this.lastComputedStyle = Object.assign(getDefaultStyle(), style);
+
+    Object.keys(style).forEach(key => {
+      Object.defineProperty(this.style, key, {
+        configurable: true,
+        enumerable: true,
+        get: () => style[key],
+        set: (value: any) => {
+          if (value === style[key] || value === undefined) {
+            return;
+          }
+
+          this.lastComputedStyle = this.computedStyle[key]
+          style[key] = value
+          this.computedStyle[key] = value
+
+          // 如果设置的是一个可缩放的属性, 计算自己
+          if (scalableStyles.includes(key) && this.style.scale) {
+            this.computedStyle[key] = value * this.style.scale
+          }
+
+          // 如果设置的是 scale, 则把所有可缩放的属性计算
+          if (key === "scale") {
+            scalableStyles.forEach(prop => {
+              if (style[prop]) {
+                this.computedStyle[prop] = style[prop] * value
+              }
+            })
+          }
+
+          if (key === "hidden") {
+            if (value) {
+              layoutAffectedStyles.forEach((key: string) => {
+                this.computedStyle[key] = 0;
+              });
+            } else {
+              layoutAffectedStyles.forEach((key: string) => {
+                this.computedStyle[key] = this.lastComputedStyle[key];
+              });
+            }
+          }
+        }
+      })
+    })
+
+    if (this.style.scale) {
+      scalableStyles.forEach((key: string) => {
+        if (this.style[key]) {
+          const computedValue = this.style[key] * this.style.scale;
+          this.computedStyle[key] = computedValue;
+        }
+      });
+    }
+
+    if (style.hidden) {
+      layoutAffectedStyles.forEach((key: string) => {
+        this.computedStyle[key] = 0;
+      });
+    }
+  }
+
+  getAbsolutePosition(element: Element) {
+    if (!element) {
+      return this.getAbsolutePosition(this)
+    }
+
+    if (!element.parent) {
+      return {
+        left: 0,
+        top: 0
+      }
+    }
+
+    const {left, top} = this.getAbsolutePosition(element.parent)
+
+    return {
+      left: left + element.layoutBox.left,
+      top: top + element.layoutBox.top
+    }
+  }
+
+  public add(element: Element) {
+    element.parent = this;
+    this.children[element.id] = element;
+  }
+
+  public remove(element?: Element) {
+    // 删除自己
+    if (!element) {
+      Object.keys(this.children).forEach(id => {
+        const child = this.children[id]
+        child.remove()
+        delete this.children[id]
+      })
+    } else if (this.children[element.id]) {
+      // 是自己的子节点才删除
+      element.remove()
+      delete this.children[element.id];
+    }
+  }
+
+  public getNodeTree(): LayoutNode {
+    return {
+      id: this.id,
+      style: this.computedStyle,
+      children: Object.keys(this.children).map((id: string) => {
+        const child = this.children[id];
+        return child.getNodeTree();
+      })
+    }
+  }
+
+  public applyLayout(layoutNode: LayoutNode) {
+    ["left", "top", "width", "height"].forEach((key: string) => {
+      if (layoutNode.layout && typeof layoutNode.layout[key] === "number") {
+        this.layoutBox[key] = layoutNode.layout[key];
+        if (this.parent && (key === "left" || key === "top")) {
+          this.layoutBox[key] += this.parent.layoutBox[key];
+        }
+      }
+    });
+
+    layoutNode.children.forEach((child: LayoutNode) => {
+      this.children[child.id].applyLayout(child);
+    });
+  }
+
+  layout() {
+    const nodeTree = this.getNodeTree();
+    computeLayout(nodeTree);
+    this.applyLayout(nodeTree);
+  }
+}
+
+export default Element;

+ 15 - 0
node_modules/widget-ui/src/event.ts

@@ -0,0 +1,15 @@
+import _EventEmitter from "eventemitter3";
+const emitter = new _EventEmitter();
+export default class EventEmitter {
+  public emit(event: string, data?: any) {
+    emitter.emit(event, data);
+  }
+  
+  public on(event: string, callback) {
+    emitter.on(event, callback);
+  }
+
+  public off(event: string, callback) {
+    emitter.off(event, callback);
+  }
+}

+ 87 - 0
node_modules/widget-ui/src/style.ts

@@ -0,0 +1,87 @@
+const textStyles: string[] = ["color", "fontSize", "textAlign", "fontWeight", "lineHeight", "lineBreak"];
+
+const scalableStyles: string[] = ["left", "top", "right", "bottom", "width", "height",
+  "margin", "marginLeft", "marginRight", "marginTop", "marginBottom",
+  "padding", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom",
+  "borderWidth", "borderLeftWidth", "borderRightWidth", "borderTopWidth", "borderBottomWidth"];
+
+const layoutAffectedStyles: string[] = [
+  "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
+  "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
+  "width", "height"];
+
+type Style = {
+  left: number,
+  top: number,
+  right: number,
+  bottom: number,
+  width: number,
+  height: number,
+  maxWidth: number,
+  maxHeight: number,
+  minWidth: number,
+  minHeight: number,
+  margin: number,
+  marginLeft: number,
+  marginRight: number,
+  marginTop: number,
+  marginBottom: number,
+  padding: number,
+  paddingLeft: number,
+  paddingRight: number,
+  paddingTop: number,
+  paddingBottom: number,
+  borderWidth: number,
+  borderLeftWidth: number,
+  borderRightWidth: number,
+  borderTopWidth: number,
+  borderBottomWidth: number,
+  flexDirection: "column" | "row",
+  justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around",
+  alignItems: "flex-start" | "center" | "flex-end" | "stretch",
+  alignSelf: "flex-start" | "center" | "flex-end" | "stretch",
+  flex: number,
+  flexWrap: "wrap" | "nowrap",
+  position: "relative" | "absolute",
+
+  hidden: boolean,
+  scale: number
+}
+
+const getDefaultStyle = () => ({
+  left: undefined,
+  top: undefined,
+  right: undefined,
+  bottom: undefined,
+  width: undefined,
+  height: undefined,
+  maxWidth: undefined,
+  maxHeight: undefined,
+  minWidth: undefined,
+  minHeight: undefined,
+  margin: undefined,
+  marginLeft: undefined,
+  marginRight: undefined,
+  marginTop: undefined,
+  marginBottom: undefined,
+  padding: undefined,
+  paddingLeft: undefined,
+  paddingRight: undefined,
+  paddingTop: undefined,
+  paddingBottom: undefined,
+  borderWidth: undefined,
+  flexDirection: undefined,
+  justifyContent: undefined,
+  alignItems: undefined,
+  alignSelf: undefined,
+  flex: undefined,
+  flexWrap: undefined,
+  position: undefined,
+
+  hidden: false,
+  scale: 1
+})
+
+export {
+  getDefaultStyle, scalableStyles, textStyles, layoutAffectedStyles
+}

+ 183 - 0
node_modules/widget-ui/test/css-layout.test.ts

@@ -0,0 +1,183 @@
+
+import Element from "../src/element";
+
+test("layout", () => {
+  const container = new Element({
+    width: 100,
+    height: 100,
+    padding: 10,
+    borderWidth: 2
+  })
+
+  const div1 = new Element({
+    left: 5,
+    top: 5,
+    width: 14,
+    height: 14
+  })
+
+  container.add(div1);
+  container.layout();
+  // css-layout 是 border-box
+  expect(container.layoutBox.left).toBe(0);
+  expect(container.layoutBox.top).toBe(0);
+  expect(container.layoutBox.width).toBe(100);
+  expect(container.layoutBox.height).toBe(100);
+
+  expect(div1.layoutBox.left).toBe(10 + 2 + 5);
+  expect(div1.layoutBox.top).toBe(10 + 2 + 5);
+  expect(div1.layoutBox.width).toBe(14);
+  expect(div1.layoutBox.height).toBe(14);
+});
+
+test("overflow", () => {
+  const container = new Element({
+    width: 100,
+    height: 100,
+    padding: 10,
+    borderWidth: 2
+  })
+
+  const div1 = new Element({
+    width: 114,
+    height: 114,
+  })
+
+  container.add(div1);
+  container.layout();
+
+  // 写死尺寸的情况下子元素不收缩父元素不撑开
+  expect(container.layoutBox.width).toBe(100);
+  expect(container.layoutBox.height).toBe(100);
+
+  expect(div1.layoutBox.left).toBe(10 + 2);
+  expect(div1.layoutBox.top).toBe(10 + 2);
+  expect(div1.layoutBox.width).toBe(114);
+  expect(div1.layoutBox.height).toBe(114);
+});
+
+test("right bottom", () => {
+  const container = new Element({
+    width: 100,
+    height: 100,
+    padding: 10,
+    borderWidth: 2
+  })
+
+  const div1 = new Element({
+    width: 14,
+    height: 14,
+    right: 13,
+    bottom: 9,
+    position: "absolute"
+  })
+
+  container.add(div1);
+  container.layout();
+
+  // right bottom 只有在 position 为 absolute 的情况下才有用
+  expect(container.layoutBox.width).toBe(100);
+  expect(container.layoutBox.height).toBe(100);
+
+  // 但这时就是以整个父元素为边界,而不是 border + padding 后的边界
+  expect(div1.layoutBox.left).toBe(100 - 13 - 14);
+  expect(div1.layoutBox.top).toBe(100 - 9 - 14);
+});
+
+test("flex center", () => {
+  const container = new Element({
+    width: 100,
+    height: 100,
+    padding: 10,
+    borderWidth: 2,
+    flexDirection: "row",
+    justifyContent: "center",
+    alignItems: "center"
+  })
+
+  const div1 = new Element({
+    width: 14,
+    height: 14
+  })
+
+  container.add(div1);
+  container.layout();
+  // 使用 flex 水平垂直居中
+  expect(div1.layoutBox.left).toBe((100 - 14)/2);
+  expect(div1.layoutBox.top).toBe((100 - 14)/2);
+})
+
+test("flex top bottom", () => {
+  const container = new Element({
+    width: 100,
+    height: 100,
+    padding: 10,
+    borderWidth: 2,
+    flexDirection: "column",
+    justifyContent: "space-between",
+    alignItems: "stretch"
+  })
+
+  // flex 实现一上一下两行水平填满
+  const div1 = new Element({
+    height: 10
+  })
+
+  const div2 = new Element({
+    height: 20
+  })
+
+  container.add(div1);
+  container.add(div2);
+  container.layout();
+
+  expect(div1.layoutBox.left).toBe(10 + 2);
+  expect(div1.layoutBox.top).toBe(10 + 2);
+  expect(div1.layoutBox.width).toBe(100 - 10*2 - 2*2);
+
+  expect(div2.layoutBox.left).toBe(10 + 2);
+  expect(div2.layoutBox.top).toBe(100 - 10 - 2 - 20);
+  expect(div2.layoutBox.width).toBe(100 - 10*2 - 2*2);
+})
+
+test("rewrite uuid", () => {
+  // 小程序为了保证 webview 和 service 侧的 coverview 不冲突,所以设置了不同的自增起点
+  // uuid 静态方法就是为了根据不同的需求去覆写
+  let uuid = 79648527;
+  Element.uuid = () =>  uuid++;
+  const container = new Element();
+  expect(container.id).toEqual(79648527);
+  const div = new Element();
+  expect(div.id).toEqual(79648528);
+});
+
+test("absolute left top", () => {
+  const container = new Element({
+    width: 300,
+    height: 200,
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center'
+  })
+  
+
+  const div1 = new Element({
+    width: 80,
+    height: 60
+  })
+  
+  const div2 = new Element({
+    width: 40,
+    height: 30
+  })
+
+  div1.add(div2)
+  container.add(div1)
+  container.layout()
+
+  expect(div1.layoutBox.left).toBe(110)
+  expect(div1.layoutBox.top).toBe(70)
+
+  expect(div2.layoutBox.left).toBe(110)
+  expect(div2.layoutBox.top).toBe(70)
+})

+ 47 - 0
node_modules/widget-ui/tsconfig.json

@@ -0,0 +1,47 @@
+{
+  "compilerOptions": {
+    "baseUrl": "src",
+    "resolveJsonModule": true,
+    "downlevelIteration": false,
+    "target": "es5",
+    "module": "commonjs",
+    "lib": [
+      "es5",
+      "es2015.promise",
+      "es2016",
+      "dom"
+    ],
+    "outDir": "./dist",
+    "paths": {
+      "@/*": [
+        "*"
+      ],
+      "*": [
+        "*"
+      ]
+    },
+    "typeRoots": [
+      "./node_modules/@types"
+    ],
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "declaration": true,
+    "stripInternal": true,
+    "experimentalDecorators": true,
+    "noImplicitReturns": true,
+    "alwaysStrict": true,
+    "noFallthroughCasesInSwitch": true,
+    "removeComments": false,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "skipLibCheck": true,
+    "pretty": true,
+    "strictPropertyInitialization": true
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 206 - 0
node_modules/widget-ui/tslint.json

@@ -0,0 +1,206 @@
+{
+  "defaultSeverity": "error",
+  "extends": [],
+  "rules": {
+    "adjacent-overload-signatures": true,
+    "align": {
+      "options": [
+        "parameters",
+        "statements"
+      ]
+    },
+    "arrow-return-shorthand": true,
+    "ban-types": {
+      "options": [
+        [
+          "Object",
+          "Avoid using the `Object` type. Did you mean `object`?"
+        ],
+        [
+          "Function",
+          "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
+        ],
+        [
+          "Boolean",
+          "Avoid using the `Boolean` type. Did you mean `boolean`?"
+        ],
+        [
+          "Number",
+          "Avoid using the `Number` type. Did you mean `number`?"
+        ],
+        [
+          "String",
+          "Avoid using the `String` type. Did you mean `string`?"
+        ],
+        [
+          "Symbol",
+          "Avoid using the `Symbol` type. Did you mean `symbol`?"
+        ]
+      ]
+    },
+    "comment-format": {
+      "options": [
+        "check-space"
+      ]
+    },
+    "curly": {
+      "options": [
+        "ignore-same-line"
+      ]
+    },
+    "cyclomatic-complexity": false,
+    "import-spacing": true,
+    "indent": {
+      "options": [
+        "spaces"
+      ]
+    },
+    "interface-over-type-literal": true,
+    "member-ordering": [
+      true,
+      {
+        "order": [
+          "public-static-field",
+          "public-instance-field",
+          "private-static-field",
+          "private-instance-field",
+          "public-constructor",
+          "private-constructor",
+          "public-instance-method",
+          "protected-instance-method",
+          "private-instance-method"
+        ],
+        "alphabetize": false
+      }
+    ],
+    "no-angle-bracket-type-assertion": true,
+    "no-arg": true,
+    "no-conditional-assignment": true,
+    "no-debugger": true,
+    "no-duplicate-super": true,
+    "no-eval": true,
+    "no-internal-module": true,
+    "no-misused-new": true,
+    "no-reference-import": true,
+    "no-string-literal": true,
+    "no-string-throw": true,
+    "no-unnecessary-initializer": true,
+    "no-unsafe-finally": true,
+    "no-unused-expression": true,
+    "no-use-before-declare": false,
+    "no-var-keyword": true,
+    "no-var-requires": true,
+    "one-line": {
+      "options": [
+        "check-catch",
+        "check-else",
+        "check-finally",
+        "check-open-brace",
+        "check-whitespace"
+      ]
+    },
+    "one-variable-per-declaration": {
+      "options": [
+        "ignore-for-loop"
+      ]
+    },
+    "ordered-imports": {
+      "options": {
+        "import-sources-order": "case-insensitive",
+        "module-source-path": "full",
+        "named-imports-order": "case-insensitive"
+      }
+    },
+    "prefer-const": true,
+    "prefer-for-of": false,
+    "quotemark": {
+      "options": [
+        "double",
+        "avoid-escape"
+      ]
+    },
+    "radix": true,
+    "semicolon": {
+      "options": [
+        "always"
+      ]
+    },
+    "space-before-function-paren": {
+      "options": {
+        "anonymous": "never",
+        "asyncArrow": "always",
+        "constructor": "never",
+        "method": "never",
+        "named": "never"
+      }
+    },
+    "trailing-comma": {
+      "options": {
+        "esSpecCompliant": true,
+        "multiline": {
+          "objects": "always",
+          "arrays": "always",
+          "functions": "always",
+          "typeLiterals": "always"
+        },
+        "singleline": "never"
+      }
+    },
+    "triple-equals": {
+      "options": [
+        "allow-null-check"
+      ]
+    },
+    "typedef": false,
+    "typedef-whitespace": {
+      "options": [
+        {
+          "call-signature": "nospace",
+          "index-signature": "nospace",
+          "parameter": "nospace",
+          "property-declaration": "nospace",
+          "variable-declaration": "nospace"
+        },
+        {
+          "call-signature": "onespace",
+          "index-signature": "onespace",
+          "parameter": "onespace",
+          "property-declaration": "onespace",
+          "variable-declaration": "onespace"
+        }
+      ]
+    },
+    "typeof-compare": false,
+    "unified-signatures": true,
+    "use-isnan": true,
+    "whitespace": {
+      "options": [
+        "check-branch",
+        "check-decl",
+        "check-operator",
+        "check-separator",
+        "check-type",
+        "check-typecast"
+      ]
+    }
+  },
+  "jsRules": {},
+  "rulesDirectory": [],
+  "no-var-requires": false,
+  "trailing-comma": [
+    true,
+    {
+      "multiline": {
+        "objects": "always",
+        "arrays": "always",
+        "functions": "always",
+        "typeLiterals": "ignore"
+      },
+      "esSpecCompliant": true
+    }
+  ],
+  "no-unused-expression": [
+    true,
+    "allow-fast-null-checks"
+  ]
+}

+ 25 - 0
node_modules/widget-ui/webpack.config.js

@@ -0,0 +1,25 @@
+const path = require("path");
+
+module.exports = {
+  mode: "production",
+  entry: path.resolve(__dirname, "src/element.ts"),
+  module: {
+    rules: [
+      {
+        test: /\.ts$/,
+        use: "ts-loader",
+        exclude: /node_modules/
+      }
+    ]
+  },
+  resolve: {
+    extensions: [".js", ".ts"]
+  },
+  output: {
+    filename: "index.js",
+    path: path.resolve(__dirname, "dist"),
+    libraryTarget: "umd", // 采用通用模块定义
+    libraryExport: "default", // 兼容 ES6(ES2015) 的模块系统、CommonJS 和 AMD 模块规范
+    globalObject: "this" // 兼容node和浏览器运行,避免window is not undefined情况
+  }
+};

+ 10 - 0
node_modules/wxml-to-canvas/.babelrc

@@ -0,0 +1,10 @@
+{
+  "plugins": [
+      ["module-resolver", {
+          "root": ["./src"],
+          "alias": {}
+      }],
+      "@babel/transform-runtime"
+  ],
+  "presets": ["@babel/preset-env"]
+}

+ 99 - 0
node_modules/wxml-to-canvas/.eslintrc.js

@@ -0,0 +1,99 @@
+module.exports = {
+  'extends': [
+    'airbnb-base',
+    'plugin:promise/recommended'
+  ],
+  'parserOptions': {
+    'ecmaVersion': 9,
+    'ecmaFeatures': {
+      'jsx': false
+    },
+    'sourceType': 'module'
+  },
+  'env': {
+    'es6': true,
+    'node': true,
+    'jest': true
+  },
+  'plugins': [
+    'import',
+    'node',
+    'promise'
+  ],
+  'rules': {
+    'arrow-parens': 'off',
+    'comma-dangle': [
+      'error',
+      'only-multiline'
+    ],
+    'complexity': ['error', 10],
+    'func-names': 'off',
+    'global-require': 'off',
+    'handle-callback-err': [
+      'error',
+      '^(err|error)$'
+    ],
+    'import/no-unresolved': [
+      'error',
+      {
+        'caseSensitive': true,
+        'commonjs': true,
+        'ignore': ['^[^.]']
+      }
+    ],
+    'import/prefer-default-export': 'off',
+    'linebreak-style': 'off',
+    'no-catch-shadow': 'error',
+    'no-continue': 'off',
+    'no-div-regex': 'warn',
+    'no-else-return': 'off',
+    'no-param-reassign': 'off',
+    'no-plusplus': 'off',
+    'no-shadow': 'off',
+    'no-multi-assign': 'off',
+    'no-underscore-dangle': 'off',
+    'node/no-deprecated-api': 'error',
+    'node/process-exit-as-throw': 'error',
+    'object-curly-spacing': [
+      'error',
+      'never'
+    ],
+    'operator-linebreak': [
+      'error',
+      'after',
+      {
+        'overrides': {
+          ':': 'before',
+          '?': 'before'
+        }
+      }
+    ],
+    'prefer-arrow-callback': 'off',
+    'prefer-destructuring': 'off',
+    'prefer-template': 'off',
+    'quote-props': [
+      1,
+      'as-needed',
+      {
+        'unnecessary': true
+      }
+    ],
+    'semi': [
+      'error',
+      'never'
+    ],
+    'no-await-in-loop': 'off',
+    'no-restricted-syntax': 'off',
+    'promise/always-return': 'off',
+  },
+  'globals': {
+    'window': true,
+    'document': true,
+    'App': true,
+    'Page': true,
+    'Component': true,
+    'Behavior': true,
+    'wx': true,
+    'getCurrentPages': true,
+  }
+}

+ 21 - 0
node_modules/wxml-to-canvas/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 wechat-miniprogram
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 187 - 0
node_modules/wxml-to-canvas/README.md

@@ -0,0 +1,187 @@
+# wxml-to-canvas
+
+[![](https://img.shields.io/npm/v/wxml-to-canvas)](https://www.npmjs.com/package/wxml-to-canvas)
+[![](https://img.shields.io/npm/l/wxml-to-canvas)](https://github.com/wechat-miniprogram/wxml-to-canvas)
+
+小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图等场景。[代码片段](https://developers.weixin.qq.com/s/r6UBlEm17pc6)
+
+
+## 使用方法
+
+#### Step1. npm 安装,参考 [小程序 npm 支持](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)
+
+```
+npm install --save wxml-to-canvas
+```
+
+#### Step2. JSON 组件声明
+
+```
+{
+  "usingComponents": {
+    "wxml-to-canvas": "wxml-to-canvas",
+  }
+}
+```
+
+#### Step3. wxml 引入组件
+
+```
+<video class="video" src="{{src}}">
+  <wxml-to-canvas class="widget"></wxml-to-canvas>
+</video>
+<image src="{{src}}" style="width: {{width}}px; height: {{height}}px"></image>
+```
+
+##### 属性列表
+
+| 属性            | 类型    | 默认值  | 必填 | 说明                   |
+| --------------- | ------- | ------- | ---- | ---------------------- |
+| width           | Number  |   400      | 否   | 画布宽度           |
+| height           | Number  |   300      | 否   | 画布高度           |
+
+
+#### Step4. js 获取实例
+
+```
+const {wxml, style} = require('./demo.js')
+Page({
+  data: {
+    src: ''
+  },
+  onLoad() {
+    this.widget = this.selectComponent('.widget')
+  },
+  renderToCanvas() {
+    const p1 = this.widget.renderToCanvas({ wxml, style })
+    p1.then((res) => {
+      this.container = res
+      this.extraImage()
+    })
+  },
+  extraImage() {
+    const p2 = this.widget.canvasToTempFilePath()
+    p2.then(res => {
+      this.setData({
+        src: res.tempFilePath,
+        width: this.container.layoutBox.width,
+        height: this.container.layoutBox.height
+      })
+    })
+  }
+})
+```
+
+## wxml 模板
+
+支持 `view`、`text`、`image` 三种标签,通过 class 匹配 style 对象中的样式。
+
+```
+<view class="container" >
+  <view class="item-box red">
+  </view>
+  <view class="item-box green" >
+    <text class="text">yeah!</text>
+  </view>
+  <view class="item-box blue">
+      <image class="img" src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3582589792,4046843010&fm=26&gp=0.jpg"></image>
+  </view>
+</view>
+```
+
+## 样式
+
+对象属性值为对应 wxml 标签的 cass 驼峰形式。**需为每个元素指定 width 和 height 属性**,否则会导致布局错误。
+
+存在多个 className 时,位置靠后的优先级更高,子元素会继承父级元素的可继承属性。
+
+元素均为 flex 布局。left/top 等 仅在 absolute 定位下生效。
+
+```
+const style = {
+  container: {
+    width: 300,
+    height: 200,
+    flexDirection: 'row',
+    justifyContent: 'space-around',
+    backgroundColor: '#ccc',
+    alignItems: 'center',
+  },
+  itemBox: {
+    width: 80,
+    height: 60,
+  },
+  red: {
+    backgroundColor: '#ff0000'
+  },
+  green: {
+    backgroundColor: '#00ff00'
+  },
+  blue: {
+    backgroundColor: '#0000ff'
+  },
+  text: {
+    width: 80,
+    height: 60,
+    textAlign: 'center',
+    verticalAlign: 'middle',
+  }
+}
+```
+
+## 接口
+
+#### f1. `renderToCanvas({wxml, style}): Promise`
+
+渲染到 canvas,传入 wxml 模板 和 style 对象,返回的容器对象包含布局和样式信息。
+
+#### f2. `canvasToTempFilePath({fileType, quality}): Promise`
+
+提取画布中容器所在区域内容生成相同大小的图片,返回临时文件地址。
+
+`fileType` 支持 `jpg`、`png` 两种格式,quality 为图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。
+
+## 支持的 css 属性
+
+### 布局相关
+
+| 属性名                | 支持的值或类型                                            | 默认值     |
+| --------------------- | --------------------------------------------------------- | ---------- |
+| width                 | number                                                    | 0          |
+| height                | number                                                    | 0          |
+| position              | relative, absolute                                        | relative   |
+| left                  | number                                                    | 0          |
+| top                   | number                                                    | 0          |
+| right                 | number                                                    | 0          |
+| bottom                | number                                                    | 0          |
+| margin                | number                                                    | 0          |
+| padding               | number                                                    | 0          |
+| borderWidth           | number                                                    | 0          |
+| borderRadius          | number                                                    | 0          |
+| flexDirection         | column, row                                               | row        |
+| flexShrink            | number                                                    | 1          |
+| flexGrow              | number                                                    |            |
+| flexWrap              | wrap, nowrap                                              | nowrap     |
+| justifyContent        | flex-start, center, flex-end, space-between, space-around | flex-start |
+| alignItems, alignSelf | flex-start, center, flex-end, stretch                     | flex-start |
+
+支持 marginLeft、paddingLeft 等
+
+### 文字
+
+| 属性名          | 支持的值或类型      | 默认值      |
+| --------------- | ------------------- | ----------- |
+| fontSize        | number              | 14          |
+| lineHeight      | number / string     | '1.4em'     |
+| textAlign       | left, center, right | left        |
+| verticalAlign   | top, middle, bottom | top         |
+| color           | string              | #000000     |
+| backgroundColor | string              | transparent |
+
+lineHeight 可取带 em 单位的字符串或数字类型。
+
+### 变形
+
+| 属性名 | 支持的值或类型 | 默认值 |
+| ------ | -------------- | ------ |
+| scale  | number         | 1      |

+ 26 - 0
node_modules/wxml-to-canvas/gulpfile.js

@@ -0,0 +1,26 @@
+const gulp = require('gulp')
+const clean = require('gulp-clean')
+
+const config = require('./tools/config')
+const BuildTask = require('./tools/build')
+const id = require('./package.json').name || 'miniprogram-custom-component'
+
+// 构建任务实例
+// eslint-disable-next-line no-new
+new BuildTask(id, config.entry)
+
+// 清空生成目录和文件
+gulp.task('clean', gulp.series(() => gulp.src(config.distPath, {read: false, allowEmpty: true}).pipe(clean()), done => {
+  if (config.isDev) {
+    return gulp.src(config.demoDist, {read: false, allowEmpty: true})
+      .pipe(clean())
+  }
+
+  return done()
+}))
+// 监听文件变化并进行开发模式构建
+gulp.task('watch', gulp.series(`${id}-watch`))
+// 开发模式构建
+gulp.task('dev', gulp.series(`${id}-dev`))
+// 生产模式构建
+gulp.task('default', gulp.series(`${id}-default`))

+ 779 - 0
node_modules/wxml-to-canvas/miniprogram_dist/index.js

@@ -0,0 +1,779 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = factory();
+	else if(typeof define === 'function' && define.amd)
+		define([], factory);
+	else {
+		var a = factory();
+		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
+	}
+})(window, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+/******/
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId]) {
+/******/ 			return installedModules[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = true;
+/******/
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/
+/******/
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+/******/
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// define __esModule on exports
+/******/ 	__webpack_require__.r = function(exports) {
+/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 		}
+/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 	};
+/******/
+/******/ 	// create a fake namespace object
+/******/ 	// mode & 1: value is a module id, require it
+/******/ 	// mode & 2: merge all properties of value into the ns
+/******/ 	// mode & 4: return value when already ns object
+/******/ 	// mode & 8|1: behave like require
+/******/ 	__webpack_require__.t = function(value, mode) {
+/******/ 		if(mode & 1) value = __webpack_require__(value);
+/******/ 		if(mode & 8) return value;
+/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ 		var ns = Object.create(null);
+/******/ 		__webpack_require__.r(ns);
+/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ 		return ns;
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 1);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+const hex = (color) => {
+  let result = null
+
+  if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
+    return color
+    // eslint-disable-next-line no-cond-assign
+  } else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
+    return '#' + result[2].split(',').map((part, index) => {
+      part = part.trim()
+      part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
+      part = part.toString(16)
+      if (part.length === 1) {
+        part = '0' + part
+      }
+      return part
+    }).join('')
+  } else {
+    return '#00000000'
+  }
+}
+
+const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
+  if (index === 0) {
+    return part
+  }
+  return part[0].toUpperCase() + part.slice(1)
+}).join('')
+
+const compareVersion = (v1, v2) => {
+  v1 = v1.split('.')
+  v2 = v2.split('.')
+  const len = Math.max(v1.length, v2.length)
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i], 10)
+    const num2 = parseInt(v2[i], 10)
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+module.exports = {
+  hex,
+  splitLineToCamelCase,
+  compareVersion
+}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+const xmlParse = __webpack_require__(2)
+const {Widget} = __webpack_require__(3)
+const {Draw} = __webpack_require__(5)
+const {compareVersion} = __webpack_require__(0)
+
+const canvasId = 'weui-canvas'
+
+Component({
+  properties: {
+    width: {
+      type: Number,
+      value: 400
+    },
+    height: {
+      type: Number,
+      value: 300
+    }
+  },
+  data: {
+    use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
+  },
+  lifetimes: {
+    attached() {
+      const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
+      const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
+      this.dpr = dpr
+      this.setData({use2dCanvas}, () => {
+        if (use2dCanvas) {
+          const query = this.createSelectorQuery()
+          query.select(`#${canvasId}`)
+            .fields({node: true, size: true})
+            .exec(res => {
+              const canvas = res[0].node
+              const ctx = canvas.getContext('2d')
+              canvas.width = res[0].width * dpr
+              canvas.height = res[0].height * dpr
+              ctx.scale(dpr, dpr)
+              this.ctx = ctx
+              this.canvas = canvas
+            })
+        } else {
+          this.ctx = wx.createCanvasContext(canvasId, this)
+        }
+      })
+    }
+  },
+  methods: {
+    async renderToCanvas(args) {
+      const {wxml, style} = args
+      const ctx = this.ctx
+      const canvas = this.canvas
+      const use2dCanvas = this.data.use2dCanvas
+
+      if (use2dCanvas && !canvas) {
+        return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
+      }
+
+      ctx.clearRect(0, 0, this.data.width, this.data.height)
+      const {root: xom} = xmlParse(wxml)
+
+      const widget = new Widget(xom, style)
+      const container = widget.init()
+      this.boundary = {
+        top: container.layoutBox.top,
+        left: container.layoutBox.left,
+        width: container.computedStyle.width,
+        height: container.computedStyle.height,
+      }
+      const draw = new Draw(ctx, canvas, use2dCanvas)
+      await draw.drawNode(container)
+
+      if (!use2dCanvas) {
+        await this.canvasDraw(ctx)
+      }
+      return Promise.resolve(container)
+    },
+
+    canvasDraw(ctx, reserve) {
+      return new Promise(resolve => {
+        ctx.draw(reserve, () => {
+          resolve()
+        })
+      })
+    },
+
+    canvasToTempFilePath(args = {}) {
+      const use2dCanvas = this.data.use2dCanvas
+
+      return new Promise((resolve, reject) => {
+        const {
+          top, left, width, height
+        } = this.boundary
+
+        const copyArgs = {
+          x: left,
+          y: top,
+          width,
+          height,
+          destWidth: width * this.dpr,
+          destHeight: height * this.dpr,
+          canvasId,
+          fileType: args.fileType || 'png',
+          quality: args.quality || 1,
+          success: resolve,
+          fail: reject
+        }
+
+        if (use2dCanvas) {
+          delete copyArgs.canvasId
+          copyArgs.canvas = this.canvas
+        }
+        wx.canvasToTempFilePath(copyArgs, this)
+      })
+    }
+  }
+})
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+
+/**
+ * Module dependencies.
+ */
+
+
+/**
+ * Expose `parse`.
+ */
+
+
+/**
+ * Parse the given string of `xml`.
+ *
+ * @param {String} xml
+ * @return {Object}
+ * @api public
+ */
+
+function parse(xml) {
+  xml = xml.trim()
+
+  // strip comments
+  xml = xml.replace(/<!--[\s\S]*?-->/g, '')
+
+  return document()
+
+  /**
+   * XML document.
+   */
+
+  function document() {
+    return {
+      declaration: declaration(),
+      root: tag()
+    }
+  }
+
+  /**
+   * Declaration.
+   */
+
+  function declaration() {
+    const m = match(/^<\?xml\s*/)
+    if (!m) return
+
+    // tag
+    const node = {
+      attributes: {}
+    }
+
+    // attributes
+    while (!(eos() || is('?>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    match(/\?>\s*/)
+
+    return node
+  }
+
+  /**
+   * Tag.
+   */
+
+  function tag() {
+    const m = match(/^<([\w-:.]+)\s*/)
+    if (!m) return
+
+    // name
+    const node = {
+      name: m[1],
+      attributes: {},
+      children: []
+    }
+
+    // attributes
+    while (!(eos() || is('>') || is('?>') || is('/>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    // self closing tag
+    if (match(/^\s*\/>\s*/)) {
+      return node
+    }
+
+    match(/\??>\s*/)
+
+    // content
+    node.content = content()
+
+    // children
+    let child
+    while (child = tag()) {
+      node.children.push(child)
+    }
+
+    // closing
+    match(/^<\/[\w-:.]+>\s*/)
+
+    return node
+  }
+
+  /**
+   * Text content.
+   */
+
+  function content() {
+    const m = match(/^([^<]*)/)
+    if (m) return m[1]
+    return ''
+  }
+
+  /**
+   * Attribute.
+   */
+
+  function attribute() {
+    const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
+    if (!m) return
+    return {name: m[1], value: strip(m[2])}
+  }
+
+  /**
+   * Strip quotes from `val`.
+   */
+
+  function strip(val) {
+    return val.replace(/^['"]|['"]$/g, '')
+  }
+
+  /**
+   * Match `re` and advance the string.
+   */
+
+  function match(re) {
+    const m = xml.match(re)
+    if (!m) return
+    xml = xml.slice(m[0].length)
+    return m
+  }
+
+  /**
+   * End-of-source.
+   */
+
+  function eos() {
+    return xml.length == 0
+  }
+
+  /**
+   * Check for `prefix`.
+   */
+
+  function is(prefix) {
+    return xml.indexOf(prefix) == 0
+  }
+}
+
+module.exports = parse
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+const Block = __webpack_require__(4)
+const {splitLineToCamelCase} = __webpack_require__(0)
+
+class Element extends Block {
+  constructor(prop) {
+    super(prop.style)
+    this.name = prop.name
+    this.attributes = prop.attributes
+  }
+}
+
+
+class Widget {
+  constructor(xom, style) {
+    this.xom = xom
+    this.style = style
+
+    this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
+  }
+
+  init() {
+    this.container = this.create(this.xom)
+    this.container.layout()
+
+    this.inheritStyle(this.container)
+    return this.container
+  }
+
+  // 继承父节点的样式
+  inheritStyle(node) {
+    const parent = node.parent || null
+    const children = node.children || {}
+    const computedStyle = node.computedStyle
+
+    if (parent) {
+      this.inheritProps.forEach(prop => {
+        computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
+      })
+    }
+
+    Object.values(children).forEach(child => {
+      this.inheritStyle(child)
+    })
+  }
+
+  create(node) {
+    let classNames = (node.attributes.class || '').split(' ')
+    classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
+    const style = {}
+    classNames.forEach(item => {
+      Object.assign(style, this.style[item] || {})
+    })
+
+    const args = {name: node.name, style}
+
+    const attrs = Object.keys(node.attributes)
+    const attributes = {}
+    for (const attr of attrs) {
+      const value = node.attributes[attr]
+      const CamelAttr = splitLineToCamelCase(attr)
+
+      if (value === '' || value === 'true') {
+        attributes[CamelAttr] = true
+      } else if (value === 'false') {
+        attributes[CamelAttr] = false
+      } else {
+        attributes[CamelAttr] = value
+      }
+    }
+    attributes.text = node.content
+    args.attributes = attributes
+    const element = new Element(args)
+    node.children.forEach(childNode => {
+      const childElement = this.create(childNode)
+      element.add(childElement)
+    })
+    return element
+  }
+}
+
+module.exports = {Widget}
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+module.exports = require("widget-ui");
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
+class Draw {
+  constructor(context, canvas, use2dCanvas = false) {
+    this.ctx = context
+    this.canvas = canvas || null
+    this.use2dCanvas = use2dCanvas
+  }
+
+  roundRect(x, y, w, h, r, fill = true, stroke = false) {
+    if (r < 0) return
+    const ctx = this.ctx
+
+    ctx.beginPath()
+    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
+    ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
+    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
+    ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
+    ctx.lineTo(x, y + r)
+    if (stroke) ctx.stroke()
+    if (fill) ctx.fill()
+  }
+
+  drawView(box, style) {
+    const ctx = this.ctx
+    const {
+      left: x, top: y, width: w, height: h
+    } = box
+    const {
+      borderRadius = 0,
+      borderWidth = 0,
+      borderColor,
+      color = '#000',
+      backgroundColor = 'transparent',
+    } = style
+    ctx.save()
+    // 外环
+    if (borderWidth > 0) {
+      ctx.fillStyle = borderColor || color
+      this.roundRect(x, y, w, h, borderRadius)
+    }
+
+    // 内环
+    ctx.fillStyle = backgroundColor
+    const innerWidth = w - 2 * borderWidth
+    const innerHeight = h - 2 * borderWidth
+    const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
+    this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
+    ctx.restore()
+  }
+
+  async drawImage(img, box, style) {
+    await new Promise((resolve, reject) => {
+      const ctx = this.ctx
+      const canvas = this.canvas
+
+      const {
+        borderRadius = 0
+      } = style
+      const {
+        left: x, top: y, width: w, height: h
+      } = box
+      ctx.save()
+      this.roundRect(x, y, w, h, borderRadius, false, false)
+      ctx.clip()
+
+      const _drawImage = (img) => {
+        if (this.use2dCanvas) {
+          const Image = canvas.createImage()
+          Image.onload = () => {
+            ctx.drawImage(Image, x, y, w, h)
+            ctx.restore()
+            resolve()
+          }
+          Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
+          Image.src = img
+        } else {
+          ctx.drawImage(img, x, y, w, h)
+          ctx.restore()
+          resolve()
+        }
+      }
+
+      const isTempFile = /^wxfile:\/\//.test(img)
+      const isNetworkFile = /^https?:\/\//.test(img)
+
+      if (isTempFile) {
+        _drawImage(img)
+      } else if (isNetworkFile) {
+        wx.downloadFile({
+          url: img,
+          success(res) {
+            if (res.statusCode === 200) {
+              _drawImage(res.tempFilePath)
+            } else {
+              reject(new Error(`downloadFile:fail ${img}`))
+            }
+          },
+          fail() {
+            reject(new Error(`downloadFile:fail ${img}`))
+          }
+        })
+      } else {
+        reject(new Error(`image format error: ${img}`))
+      }
+    })
+  }
+
+  // eslint-disable-next-line complexity
+  drawText(text, box, style) {
+    const ctx = this.ctx
+    let {
+      left: x, top: y, width: w, height: h
+    } = box
+    let {
+      color = '#000',
+      lineHeight = '1.4em',
+      fontSize = 14,
+      textAlign = 'left',
+      verticalAlign = 'top',
+      backgroundColor = 'transparent'
+    } = style
+
+    if (typeof lineHeight === 'string') { // 2em
+      lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
+    }
+    if (!text || (lineHeight > h)) return
+
+    ctx.save()
+    ctx.textBaseline = 'top'
+    ctx.font = `${fontSize}px sans-serif`
+    ctx.textAlign = textAlign
+
+    // 背景色
+    ctx.fillStyle = backgroundColor
+    this.roundRect(x, y, w, h, 0)
+
+    // 文字颜色
+    ctx.fillStyle = color
+
+    // 水平布局
+    switch (textAlign) {
+      case 'left':
+        break
+      case 'center':
+        x += 0.5 * w
+        break
+      case 'right':
+        x += w
+        break
+      default: break
+    }
+
+    const textWidth = ctx.measureText(text).width
+    const actualHeight = Math.ceil(textWidth / w) * lineHeight
+    let paddingTop = Math.ceil((h - actualHeight) / 2)
+    if (paddingTop < 0) paddingTop = 0
+
+    // 垂直布局
+    switch (verticalAlign) {
+      case 'top':
+        break
+      case 'middle':
+        y += paddingTop
+        break
+      case 'bottom':
+        y += 2 * paddingTop
+        break
+      default: break
+    }
+
+    const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
+
+    // 不超过一行
+    if (textWidth <= w) {
+      ctx.fillText(text, x, y + inlinePaddingTop)
+      return
+    }
+
+    // 多行文本
+    const chars = text.split('')
+    const _y = y
+
+    // 逐行绘制
+    let line = ''
+    for (const ch of chars) {
+      const testLine = line + ch
+      const testWidth = ctx.measureText(testLine).width
+
+      if (testWidth > w) {
+        ctx.fillText(line, x, y + inlinePaddingTop)
+        y += lineHeight
+        line = ch
+        if ((y + lineHeight) > (_y + h)) break
+      } else {
+        line = testLine
+      }
+    }
+
+    // 避免溢出
+    if ((y + lineHeight) <= (_y + h)) {
+      ctx.fillText(line, x, y + inlinePaddingTop)
+    }
+    ctx.restore()
+  }
+
+  async drawNode(element) {
+    const {layoutBox, computedStyle, name} = element
+    const {src, text} = element.attributes
+    if (name === 'view') {
+      this.drawView(layoutBox, computedStyle)
+    } else if (name === 'image') {
+      await this.drawImage(src, layoutBox, computedStyle)
+    } else if (name === 'text') {
+      this.drawText(text, layoutBox, computedStyle)
+    }
+    const childs = Object.values(element.children)
+    for (const child of childs) {
+      await this.drawNode(child)
+    }
+  }
+}
+
+
+module.exports = {
+  Draw
+}
+
+
+/***/ })
+/******/ ]);
+});

+ 4 - 0
node_modules/wxml-to-canvas/miniprogram_dist/index.json

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

+ 2 - 0
node_modules/wxml-to-canvas/miniprogram_dist/index.wxml

@@ -0,0 +1,2 @@
+<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
+<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

+ 0 - 0
node_modules/wxml-to-canvas/miniprogram_dist/index.wxss


+ 57 - 0
node_modules/wxml-to-canvas/miniprogram_dist/utils.js

@@ -0,0 +1,57 @@
+const hex = (color) => {
+  let result = null
+
+  if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
+    return color
+    // eslint-disable-next-line no-cond-assign
+  } else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
+    return '#' + result[2].split(',').map((part, index) => {
+      part = part.trim()
+      part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
+      part = part.toString(16)
+      if (part.length === 1) {
+        part = '0' + part
+      }
+      return part
+    }).join('')
+  } else {
+    return '#00000000'
+  }
+}
+
+const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
+  if (index === 0) {
+    return part
+  }
+  return part[0].toUpperCase() + part.slice(1)
+}).join('')
+
+const compareVersion = (v1, v2) => {
+  v1 = v1.split('.')
+  v2 = v2.split('.')
+  const len = Math.max(v1.length, v2.length)
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i], 10)
+    const num2 = parseInt(v2[i], 10)
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+module.exports = {
+  hex,
+  splitLineToCamelCase,
+  compareVersion
+}

+ 63 - 0
node_modules/wxml-to-canvas/package.json

@@ -0,0 +1,63 @@
+{
+  "name": "wxml-to-canvas",
+  "version": "1.1.1",
+  "description": "",
+  "main": "miniprogram_dist/index.js",
+  "scripts": {
+    "dev": "gulp dev --develop",
+    "watch": "gulp watch --develop --watch",
+    "build": "gulp",
+    "dist": "npm run build",
+    "clean-dev": "gulp clean --develop",
+    "clean": "gulp clean",
+    "test": "jest --bail",
+    "test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --bail",
+    "coverage": "jest ./test/* --coverage --bail",
+    "lint": "eslint \"src/**/*.js\" --fix",
+    "lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\" --fix"
+  },
+  "miniprogram": "miniprogram_dist",
+  "jest": {
+    "testEnvironment": "jsdom",
+    "testURL": "https://jest.test",
+    "collectCoverageFrom": [
+      "src/**/*.js"
+    ],
+    "moduleDirectories": [
+      "node_modules",
+      "src"
+    ]
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "sanfordsun",
+  "license": "MIT",
+  "devDependencies": {
+    "colors": "^1.3.1",
+    "eslint": "^5.14.1",
+    "eslint-config-airbnb-base": "13.1.0",
+    "eslint-loader": "^2.1.2",
+    "eslint-plugin-import": "^2.16.0",
+    "eslint-plugin-node": "^7.0.1",
+    "eslint-plugin-promise": "^3.8.0",
+    "gulp": "^4.0.0",
+    "gulp-clean": "^0.4.0",
+    "gulp-if": "^2.0.2",
+    "gulp-install": "^1.1.0",
+    "gulp-less": "^4.0.1",
+    "gulp-rename": "^1.4.0",
+    "gulp-sourcemaps": "^2.6.5",
+    "jest": "^23.5.0",
+    "miniprogram-simulate": "^1.0.0",
+    "through2": "^2.0.3",
+    "vinyl": "^2.2.0",
+    "webpack": "^4.29.5",
+    "webpack-cli": "^3.3.10",
+    "webpack-node-externals": "^1.7.2"
+  },
+  "dependencies": {
+    "widget-ui": "^1.0.2"
+  }
+}

+ 225 - 0
node_modules/wxml-to-canvas/src/draw.js

@@ -0,0 +1,225 @@
+class Draw {
+  constructor(context, canvas, use2dCanvas = false) {
+    this.ctx = context
+    this.canvas = canvas || null
+    this.use2dCanvas = use2dCanvas
+  }
+
+  roundRect(x, y, w, h, r, fill = true, stroke = false) {
+    if (r < 0) return
+    const ctx = this.ctx
+
+    ctx.beginPath()
+    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
+    ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
+    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
+    ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
+    ctx.lineTo(x, y + r)
+    if (stroke) ctx.stroke()
+    if (fill) ctx.fill()
+  }
+
+  drawView(box, style) {
+    const ctx = this.ctx
+    const {
+      left: x, top: y, width: w, height: h
+    } = box
+    const {
+      borderRadius = 0,
+      borderWidth = 0,
+      borderColor,
+      color = '#000',
+      backgroundColor = 'transparent',
+    } = style
+    ctx.save()
+    // 外环
+    if (borderWidth > 0) {
+      ctx.fillStyle = borderColor || color
+      this.roundRect(x, y, w, h, borderRadius)
+    }
+
+    // 内环
+    ctx.fillStyle = backgroundColor
+    const innerWidth = w - 2 * borderWidth
+    const innerHeight = h - 2 * borderWidth
+    const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
+    this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
+    ctx.restore()
+  }
+
+  async drawImage(img, box, style) {
+    await new Promise((resolve, reject) => {
+      const ctx = this.ctx
+      const canvas = this.canvas
+
+      const {
+        borderRadius = 0
+      } = style
+      const {
+        left: x, top: y, width: w, height: h
+      } = box
+      ctx.save()
+      this.roundRect(x, y, w, h, borderRadius, false, false)
+      ctx.clip()
+
+      const _drawImage = (img) => {
+        if (this.use2dCanvas) {
+          const Image = canvas.createImage()
+          Image.onload = () => {
+            ctx.drawImage(Image, x, y, w, h)
+            ctx.restore()
+            resolve()
+          }
+          Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
+          Image.src = img
+        } else {
+          ctx.drawImage(img, x, y, w, h)
+          ctx.restore()
+          resolve()
+        }
+      }
+
+      const isTempFile = /^wxfile:\/\//.test(img)
+      const isNetworkFile = /^https?:\/\//.test(img)
+
+      if (isTempFile) {
+        _drawImage(img)
+      } else if (isNetworkFile) {
+        wx.downloadFile({
+          url: img,
+          success(res) {
+            if (res.statusCode === 200) {
+              _drawImage(res.tempFilePath)
+            } else {
+              reject(new Error(`downloadFile:fail ${img}`))
+            }
+          },
+          fail() {
+            reject(new Error(`downloadFile:fail ${img}`))
+          }
+        })
+      } else {
+        reject(new Error(`image format error: ${img}`))
+      }
+    })
+  }
+
+  // eslint-disable-next-line complexity
+  drawText(text, box, style) {
+    const ctx = this.ctx
+    let {
+      left: x, top: y, width: w, height: h
+    } = box
+    let {
+      color = '#000',
+      lineHeight = '1.4em',
+      fontSize = 14,
+      textAlign = 'left',
+      verticalAlign = 'top',
+      backgroundColor = 'transparent'
+    } = style
+
+    if (typeof lineHeight === 'string') { // 2em
+      lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
+    }
+    if (!text || (lineHeight > h)) return
+
+    ctx.save()
+    ctx.textBaseline = 'top'
+    ctx.font = `${fontSize}px sans-serif`
+    ctx.textAlign = textAlign
+
+    // 背景色
+    ctx.fillStyle = backgroundColor
+    this.roundRect(x, y, w, h, 0)
+
+    // 文字颜色
+    ctx.fillStyle = color
+
+    // 水平布局
+    switch (textAlign) {
+      case 'left':
+        break
+      case 'center':
+        x += 0.5 * w
+        break
+      case 'right':
+        x += w
+        break
+      default: break
+    }
+
+    const textWidth = ctx.measureText(text).width
+    const actualHeight = Math.ceil(textWidth / w) * lineHeight
+    let paddingTop = Math.ceil((h - actualHeight) / 2)
+    if (paddingTop < 0) paddingTop = 0
+
+    // 垂直布局
+    switch (verticalAlign) {
+      case 'top':
+        break
+      case 'middle':
+        y += paddingTop
+        break
+      case 'bottom':
+        y += 2 * paddingTop
+        break
+      default: break
+    }
+
+    const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
+
+    // 不超过一行
+    if (textWidth <= w) {
+      ctx.fillText(text, x, y + inlinePaddingTop)
+      return
+    }
+
+    // 多行文本
+    const chars = text.split('')
+    const _y = y
+
+    // 逐行绘制
+    let line = ''
+    for (const ch of chars) {
+      const testLine = line + ch
+      const testWidth = ctx.measureText(testLine).width
+
+      if (testWidth > w) {
+        ctx.fillText(line, x, y + inlinePaddingTop)
+        y += lineHeight
+        line = ch
+        if ((y + lineHeight) > (_y + h)) break
+      } else {
+        line = testLine
+      }
+    }
+
+    // 避免溢出
+    if ((y + lineHeight) <= (_y + h)) {
+      ctx.fillText(line, x, y + inlinePaddingTop)
+    }
+    ctx.restore()
+  }
+
+  async drawNode(element) {
+    const {layoutBox, computedStyle, name} = element
+    const {src, text} = element.attributes
+    if (name === 'view') {
+      this.drawView(layoutBox, computedStyle)
+    } else if (name === 'image') {
+      await this.drawImage(src, layoutBox, computedStyle)
+    } else if (name === 'text') {
+      this.drawText(text, layoutBox, computedStyle)
+    }
+    const childs = Object.values(element.children)
+    for (const child of childs) {
+      await this.drawNode(child)
+    }
+  }
+}
+
+
+module.exports = {
+  Draw
+}

+ 117 - 0
node_modules/wxml-to-canvas/src/index.js

@@ -0,0 +1,117 @@
+
+const xmlParse = require('./xml-parser')
+const {Widget} = require('./widget')
+const {Draw} = require('./draw')
+const {compareVersion} = require('./utils')
+
+const canvasId = 'weui-canvas'
+
+Component({
+  properties: {
+    width: {
+      type: Number,
+      value: 400
+    },
+    height: {
+      type: Number,
+      value: 300
+    }
+  },
+  data: {
+    use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
+  },
+  lifetimes: {
+    attached() {
+      const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
+      const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
+      this.dpr = dpr
+      this.setData({use2dCanvas}, () => {
+        if (use2dCanvas) {
+          const query = this.createSelectorQuery()
+          query.select(`#${canvasId}`)
+            .fields({node: true, size: true})
+            .exec(res => {
+              const canvas = res[0].node
+              const ctx = canvas.getContext('2d')
+              canvas.width = res[0].width * dpr
+              canvas.height = res[0].height * dpr
+              ctx.scale(dpr, dpr)
+              this.ctx = ctx
+              this.canvas = canvas
+            })
+        } else {
+          this.ctx = wx.createCanvasContext(canvasId, this)
+        }
+      })
+    }
+  },
+  methods: {
+    async renderToCanvas(args) {
+      const {wxml, style} = args
+      const ctx = this.ctx
+      const canvas = this.canvas
+      const use2dCanvas = this.data.use2dCanvas
+
+      if (use2dCanvas && !canvas) {
+        return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
+      }
+
+      ctx.clearRect(0, 0, this.data.width, this.data.height)
+      const {root: xom} = xmlParse(wxml)
+
+      const widget = new Widget(xom, style)
+      const container = widget.init()
+      this.boundary = {
+        top: container.layoutBox.top,
+        left: container.layoutBox.left,
+        width: container.computedStyle.width,
+        height: container.computedStyle.height,
+      }
+      const draw = new Draw(ctx, canvas, use2dCanvas)
+      await draw.drawNode(container)
+
+      if (!use2dCanvas) {
+        await this.canvasDraw(ctx)
+      }
+      return Promise.resolve(container)
+    },
+
+    canvasDraw(ctx, reserve) {
+      return new Promise(resolve => {
+        ctx.draw(reserve, () => {
+          resolve()
+        })
+      })
+    },
+
+    canvasToTempFilePath(args = {}) {
+      const use2dCanvas = this.data.use2dCanvas
+
+      return new Promise((resolve, reject) => {
+        const {
+          top, left, width, height
+        } = this.boundary
+
+        const copyArgs = {
+          x: left,
+          y: top,
+          width,
+          height,
+          destWidth: width * this.dpr,
+          destHeight: height * this.dpr,
+          canvasId,
+          fileType: args.fileType || 'png',
+          quality: args.quality || 1,
+          success: resolve,
+          fail: reject
+        }
+
+        if (use2dCanvas) {
+          delete copyArgs.canvasId
+          copyArgs.canvas = this.canvas
+        }
+        wx.canvasToTempFilePath(copyArgs, this)
+      })
+    }
+  }
+})

+ 4 - 0
node_modules/wxml-to-canvas/src/index.json

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

+ 2 - 0
node_modules/wxml-to-canvas/src/index.wxml

@@ -0,0 +1,2 @@
+<canvas wx:if="{{use2dCanvas}}" id="weui-canvas" type="2d" style="width: {{width}}px; height: {{height}}px;"></canvas>
+<canvas wx:else canvas-id="weui-canvas" style="width: {{width}}px; height: {{height}}px;"></canvas>

+ 0 - 0
node_modules/wxml-to-canvas/src/index.wxss


+ 57 - 0
node_modules/wxml-to-canvas/src/utils.js

@@ -0,0 +1,57 @@
+const hex = (color) => {
+  let result = null
+
+  if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
+    return color
+    // eslint-disable-next-line no-cond-assign
+  } else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
+    return '#' + result[2].split(',').map((part, index) => {
+      part = part.trim()
+      part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
+      part = part.toString(16)
+      if (part.length === 1) {
+        part = '0' + part
+      }
+      return part
+    }).join('')
+  } else {
+    return '#00000000'
+  }
+}
+
+const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
+  if (index === 0) {
+    return part
+  }
+  return part[0].toUpperCase() + part.slice(1)
+}).join('')
+
+const compareVersion = (v1, v2) => {
+  v1 = v1.split('.')
+  v2 = v2.split('.')
+  const len = Math.max(v1.length, v2.length)
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i], 10)
+    const num2 = parseInt(v2[i], 10)
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+module.exports = {
+  hex,
+  splitLineToCamelCase,
+  compareVersion
+}

+ 81 - 0
node_modules/wxml-to-canvas/src/widget.js

@@ -0,0 +1,81 @@
+const Block = require('widget-ui')
+const {splitLineToCamelCase} = require('./utils')
+
+class Element extends Block {
+  constructor(prop) {
+    super(prop.style)
+    this.name = prop.name
+    this.attributes = prop.attributes
+  }
+}
+
+
+class Widget {
+  constructor(xom, style) {
+    this.xom = xom
+    this.style = style
+
+    this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
+  }
+
+  init() {
+    this.container = this.create(this.xom)
+    this.container.layout()
+
+    this.inheritStyle(this.container)
+    return this.container
+  }
+
+  // 继承父节点的样式
+  inheritStyle(node) {
+    const parent = node.parent || null
+    const children = node.children || {}
+    const computedStyle = node.computedStyle
+
+    if (parent) {
+      this.inheritProps.forEach(prop => {
+        computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
+      })
+    }
+
+    Object.values(children).forEach(child => {
+      this.inheritStyle(child)
+    })
+  }
+
+  create(node) {
+    let classNames = (node.attributes.class || '').split(' ')
+    classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
+    const style = {}
+    classNames.forEach(item => {
+      Object.assign(style, this.style[item] || {})
+    })
+
+    const args = {name: node.name, style}
+
+    const attrs = Object.keys(node.attributes)
+    const attributes = {}
+    for (const attr of attrs) {
+      const value = node.attributes[attr]
+      const CamelAttr = splitLineToCamelCase(attr)
+
+      if (value === '' || value === 'true') {
+        attributes[CamelAttr] = true
+      } else if (value === 'false') {
+        attributes[CamelAttr] = false
+      } else {
+        attributes[CamelAttr] = value
+      }
+    }
+    attributes.text = node.content
+    args.attributes = attributes
+    const element = new Element(args)
+    node.children.forEach(childNode => {
+      const childElement = this.create(childNode)
+      element.add(childElement)
+    })
+    return element
+  }
+}
+
+module.exports = {Widget}

+ 164 - 0
node_modules/wxml-to-canvas/src/xml-parser.js

@@ -0,0 +1,164 @@
+
+/**
+ * Module dependencies.
+ */
+
+
+/**
+ * Expose `parse`.
+ */
+
+
+/**
+ * Parse the given string of `xml`.
+ *
+ * @param {String} xml
+ * @return {Object}
+ * @api public
+ */
+
+function parse(xml) {
+  xml = xml.trim()
+
+  // strip comments
+  xml = xml.replace(/<!--[\s\S]*?-->/g, '')
+
+  return document()
+
+  /**
+   * XML document.
+   */
+
+  function document() {
+    return {
+      declaration: declaration(),
+      root: tag()
+    }
+  }
+
+  /**
+   * Declaration.
+   */
+
+  function declaration() {
+    const m = match(/^<\?xml\s*/)
+    if (!m) return
+
+    // tag
+    const node = {
+      attributes: {}
+    }
+
+    // attributes
+    while (!(eos() || is('?>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    match(/\?>\s*/)
+
+    return node
+  }
+
+  /**
+   * Tag.
+   */
+
+  function tag() {
+    const m = match(/^<([\w-:.]+)\s*/)
+    if (!m) return
+
+    // name
+    const node = {
+      name: m[1],
+      attributes: {},
+      children: []
+    }
+
+    // attributes
+    while (!(eos() || is('>') || is('?>') || is('/>'))) {
+      const attr = attribute()
+      if (!attr) return node
+      node.attributes[attr.name] = attr.value
+    }
+
+    // self closing tag
+    if (match(/^\s*\/>\s*/)) {
+      return node
+    }
+
+    match(/\??>\s*/)
+
+    // content
+    node.content = content()
+
+    // children
+    let child
+    while (child = tag()) {
+      node.children.push(child)
+    }
+
+    // closing
+    match(/^<\/[\w-:.]+>\s*/)
+
+    return node
+  }
+
+  /**
+   * Text content.
+   */
+
+  function content() {
+    const m = match(/^([^<]*)/)
+    if (m) return m[1]
+    return ''
+  }
+
+  /**
+   * Attribute.
+   */
+
+  function attribute() {
+    const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
+    if (!m) return
+    return {name: m[1], value: strip(m[2])}
+  }
+
+  /**
+   * Strip quotes from `val`.
+   */
+
+  function strip(val) {
+    return val.replace(/^['"]|['"]$/g, '')
+  }
+
+  /**
+   * Match `re` and advance the string.
+   */
+
+  function match(re) {
+    const m = xml.match(re)
+    if (!m) return
+    xml = xml.slice(m[0].length)
+    return m
+  }
+
+  /**
+   * End-of-source.
+   */
+
+  function eos() {
+    return xml.length == 0
+  }
+
+  /**
+   * Check for `prefix`.
+   */
+
+  function is(prefix) {
+    return xml.indexOf(prefix) == 0
+  }
+}
+
+module.exports = parse

+ 40 - 0
package-lock.json

@@ -0,0 +1,40 @@
+{
+  "name": "swk",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "swk",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "wxml-to-canvas": "^1.1.1"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
+    "node_modules/widget-ui": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/widget-ui/-/widget-ui-1.0.2.tgz",
+      "integrity": "sha512-gDXosr5mflJdMA1weU1A47aTsTFfMJhfA4EKgO5XFebY3eVklf80KD4GODfrjo8J2WQ+9YjL1Rd9UUmKIzhShw==",
+      "license": "ISC",
+      "dependencies": {
+        "eventemitter3": "^4.0.0"
+      }
+    },
+    "node_modules/wxml-to-canvas": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/wxml-to-canvas/-/wxml-to-canvas-1.1.1.tgz",
+      "integrity": "sha512-3mDjHzujY/UgdCOXij/MnmwJYerVjwkyQHMBFBE8zh89DK7h7UTzoydWFqEBjIC0rfZM+AXl5kDh9hUcsNpSmg==",
+      "license": "MIT",
+      "dependencies": {
+        "widget-ui": "^1.0.2"
+      }
+    }
+  }
+}

+ 14 - 0
package.json

@@ -0,0 +1,14 @@
+{
+  "name": "swk",
+  "version": "1.0.0",
+  "main": "app.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "wxml-to-canvas": "^1.1.1"
+  }
+}

+ 1 - 1
pages/index/index.js

@@ -214,7 +214,7 @@ VueLikePage([], {
       //   currentScene:tmp[0].id
       // })
     },
-
+    // 横琴是ZHS2409020-1,替换
     getData(prjId = "ZHS2305758-1", scenes) {
       this.setData({
         cdn_url: CDN_URL + "/" + prjId,

+ 400 - 2
pages/work/index.js

@@ -12,6 +12,8 @@ VueLikePage([], {
     type: "",
     loadCompele: false,
     filePath: "",
+    projectid: '',
+    isEditing: false,
     info: {
       resourceImg: {},
       banner: {},
@@ -20,11 +22,41 @@ VueLikePage([], {
       rescan: {},
       activeSceneBdImg: {},
     },
+    isZoom: false,
+    ipsImgList: [],
+    selectedIp: null,
+    selectedIpIndex: -1,
+    ipScaleX: 1,
+    ipScaleY: 1,
+    ipRotate: 0,
+    ipConfirmed: false,
+    ipLeft: 0,
+    ipTop: 0,
+    positionInitialized: false,
+    canvasWidth: 375,
+    canvasHeight: 600,
+    widgetVisible: true,
+    zoomScrollLeft: 0,
+    scaleOrientation: ''
   },
   methods: {
+    zoom() {
+      this.setData({
+        isZoom: !this.data.isZoom,
+        selectedIp: null,
+        selectedIpIndex: -1,
+        ipScaleX: 1,
+        ipScaleY: 1,
+        ipRotate: 0,
+        ipConfirmed: false,
+      });
+    },
     onLoad: function (options) {
       let { rdw, id, type, projectid } = options;
-
+      if(!projectid){
+        projectid = 'ZHS2409020-1';
+      }
+      this.initIpsList(projectid);
       this.getData(projectid);
 
       let link = "";
@@ -32,12 +64,14 @@ VueLikePage([], {
         link = `${VIDEO_BASE_URL}4dvedio/${rdw}.mp4`;
       } else {
         link = `${VIDEO_BASE_URL}4dpic/${rdw}.jpg`;
+        // link='https://4dkk.4dage.com/fusion/test/file/797098a37e0c4588ae88f2ec4b1f12df.png'
       }
 
       this.setData({
         url_link: link,
         id,
         type,
+        projectid,
         cdn_url: CDN_URL + "/" + projectid,
       });
 
@@ -47,6 +81,18 @@ VueLikePage([], {
         });
       });
     },
+    initIpsList(projectid) {
+      const list = [];
+      for (let i = 1; i <= 42; i++) {
+        list.push({
+          name: String(i),
+          imgUrl: `${CDN_URL}/${projectid}/ip/${i}.png`,
+        });
+      }
+      this.setData({
+        ipsImgList: list,
+      });
+    },
     loadcompele() {
       this.setData({
         loadCompele: true,
@@ -59,6 +105,11 @@ VueLikePage([], {
 
       wx.navigateBack();
     },
+    edit() {
+      this.setData({
+        isEditing: !this.data.isEditing,
+      });
+    },
 
     downloadF(cb = () => {}) {
       let link = this.data.url_link,
@@ -99,8 +150,178 @@ VueLikePage([], {
       });
     },
 
+    async generateImage() {
+      wx.showLoading({ title: '生成中...', mask: true });
+      try {
+        const query = wx.createSelectorQuery();
+        query.select('.w_video').boundingClientRect();
+
+        if (this.data.selectedIp) {
+            query.select('.ip-overlay').boundingClientRect();
+        }
+        
+        const res = await new Promise(resolve => query.exec(resolve));
+        const container = res[0];
+        const overlay = this.data.selectedIp ? res[1] : null;
+        
+        if (!container) throw new Error('Cannot find container');
+        
+        let width = container.width;
+        let height = container.height;
+
+        if (this.data.isZoom) {
+          const imgRes = await new Promise((resolve, reject) => {
+            wx.getImageInfo({
+              src: this.data.url_link,
+              success: resolve,
+              fail: reject
+            })
+          });
+          width = imgRes.width;
+          height = imgRes.height;
+        }
+
+        // Limit canvas size to avoid incomplete rendering on some devices
+        const dpr = wx.getSystemInfoSync().pixelRatio;
+        const maxCanvasSize = 4096; 
+        let scale = 1;
+        
+        if (width * dpr > maxCanvasSize || height * dpr > maxCanvasSize) {
+            scale = Math.min(maxCanvasSize / (width * dpr), maxCanvasSize / (height * dpr));
+        }
+
+        const canvasWidth = width * scale;
+        const canvasHeight = height * scale;
+        
+        await this._resetWidget(canvasWidth, canvasHeight);
+        const widget = this.selectComponent('#widget');
+
+        const mainUrl = this.data.url_link;
+        let wxml = '';
+
+        if (this.data.isZoom) {
+          wxml = `
+            <view class="container">
+              <image class="main" src="${mainUrl}"></image>
+            </view>
+            `;
+        } else {
+          const bgUrl = this.data.info.resourceImg.bg ? (this.data.cdn_url + this.data.info.resourceImg.bg) : '';
+          wxml = `
+                <view class="container">
+                <image class="bg" src="${bgUrl}"></image>
+                ${(this.data.type != '0') ? `<image class="main" src="${mainUrl}"></image>` : ''}
+                </view>
+              `;
+        }
+
+        const style = {
+          container: { width: canvasWidth, height: canvasHeight, position: 'relative', overflow: 'hidden' },
+          bg: { width: canvasWidth, height: canvasHeight, position: 'absolute', left: 0, top: 0 },
+          main: { width: canvasWidth, height: canvasHeight, position: 'absolute', left: 0, top: 0 }
+        };
+
+        await widget.renderToCanvas({ wxml, style });
+
+        if (this.data.selectedIp && overlay) {
+            const ctx = widget.ctx;
+            const use2d = widget.data.use2dCanvas;
+            const stickerUrl = this.data.selectedIp.imgUrl;
+            
+            // Get local path
+            const stickerInfo = await new Promise((resolve, reject) => {
+                wx.getImageInfo({
+                    src: stickerUrl,
+                    success: resolve,
+                    fail: reject
+                })
+            });
+            
+            let imgToDraw = stickerInfo.path;
+            if (use2d) {
+                const canvas = widget.canvas;
+                const img = canvas.createImage();
+                await new Promise((resolve, reject) => {
+                    img.onload = resolve;
+                    img.onerror = reject;
+                    img.src = stickerInfo.path;
+                });
+                imgToDraw = img;
+            }
+            
+            const ratio = canvasWidth / container.width;
+            const cx = ((overlay.left - container.left + overlay.width / 2) + (this.data.isZoom ? this.data.zoomScrollLeft : 0)) * ratio;
+            const cy = (overlay.top - container.top + overlay.height / 2) * ratio;
+            const overlayWidth = overlay.width * ratio;
+            const overlayHeight = overlay.height * ratio;
+            
+            ctx.save();
+            ctx.translate(cx, cy);
+            ctx.rotate(this.data.ipRotate * Math.PI / 180);
+            ctx.scale(this.data.ipScaleX, this.data.ipScaleY);
+            ctx.drawImage(imgToDraw, -overlayWidth / 2, -overlayHeight / 2, overlayWidth, overlayHeight);
+            ctx.restore();
+            
+            // If legacy, might need draw()
+            if (!use2d) {
+                await new Promise(resolve => ctx.draw(true, resolve));
+            }
+        }
+        
+        const { tempFilePath } = await widget.canvasToTempFilePath();
+        
+        // Save
+        wx.saveImageToPhotosAlbum({
+            filePath: tempFilePath,
+            success: () => {
+                wx.showModal({
+                    title: "提示",
+                    content: "已保存到相册,快去分享吧",
+                    showCancel: false,
+                });
+            },
+            fail: (e) => {
+                if (!(e.errMsg.indexOf("cancel") > -1)) {
+                    wx.showModal({
+                        title: "提示",
+                        content: "保存失败,请检查是否开启相册保存权限",
+                        showCancel: false,
+                    });
+                }
+            }
+        });
+
+      } catch (e) {
+        console.error(e);
+        wx.showToast({ title: '生成失败', icon: 'none' });
+      } finally {
+        wx.hideLoading();
+      }
+    },
+    async _resetWidget(width, height) {
+      this.setData({ widgetVisible: false });
+      await new Promise(r => setTimeout(r, 50));
+      this.setData({ canvasWidth: width, canvasHeight: height, widgetVisible: true });
+      await new Promise(r => setTimeout(r, 120));
+    },
+    onZoomScroll(e) {
+      this.setData({ zoomScrollLeft: e.detail.scrollLeft || 0 });
+    },
+
     saveAlbum() {
       let type = this.data.type;
+      
+      if (this.data.projectid == 'ZHS2409020-1') {
+         if (this.data.selectedIp && !this.data.ipConfirmed) {
+           wx.showToast({
+             title: '请先确认标签',
+             icon: 'none'
+           })
+           return;
+         }
+         this.generateImage();
+         return;
+      }
 
       wx.showLoading({
         title: "保存中…",
@@ -136,6 +357,7 @@ VueLikePage([], {
       }
     },
 
+    // 横琴是ZHS2409020-1,替换
     getData(prjId = "ZHS2305758-1") {
       wx.showLoading({
         title: "资源加载中",
@@ -148,7 +370,7 @@ VueLikePage([], {
       wx.request({
         url: `${VIDEO_BASE_URL}project/4dage-sxb/${prjId}/config.json`,
         success: ({ data: { title, ...rest } }) => {
-          this.setData(
+          this.setData( 
             {
               info: rest,
             },
@@ -162,5 +384,181 @@ VueLikePage([], {
         },
       });
     },
+    selectIp(e) {
+      const index = e.currentTarget.dataset.index;
+      const item = this.data.ipsImgList[index];
+      if (!item) return;
+      this.setData({
+        selectedIpIndex: index,
+        selectedIp: item,
+        ipScaleX: 1,
+        ipScaleY: 1,
+        ipRotate: 0,
+        ipConfirmed: false,
+        positionInitialized: false,
+      }, () => {
+        this.getOverlayRect();
+        
+        // Initialize position
+        const query = wx.createSelectorQuery();
+        query.select('.w_video').boundingClientRect();
+        query.select('.ip-overlay').boundingClientRect();
+        query.exec((res) => {
+            const container = res[0];
+            const overlay = res[1];
+            if (container && overlay) {
+                this.setData({
+                    ipLeft: overlay.left - container.left,
+                    ipTop: overlay.top - container.top,
+                    positionInitialized: true
+                });
+            }
+        });
+      });
+    },
+
+    dragStart(e) {
+      if (this.data.ipConfirmed) return;
+      const touch = e.touches[0];
+      this.setData({
+        dragStartX: touch.clientX,
+        dragStartY: touch.clientY,
+        startIpLeft: this.data.ipLeft,
+        startIpTop: this.data.ipTop
+      });
+    },
+
+    dragMove(e) {
+      if (this.data.ipConfirmed) return;
+      const touch = e.touches[0];
+      const dx = touch.clientX - this.data.dragStartX;
+      const dy = touch.clientY - this.data.dragStartY;
+      
+      this.setData({
+        ipLeft: this.data.startIpLeft + dx,
+        ipTop: this.data.startIpTop + dy
+      });
+    },
+
+    getOverlayRect() {
+      const query = wx.createSelectorQuery();
+      query.select('.ip-overlay').boundingClientRect(rect => {
+        if (rect) {
+          this.setData({
+            centerX: rect.left + rect.width / 2,
+            centerY: rect.top + rect.height / 2
+          });
+        }
+      }).exec();
+    },
+
+    rotateStart(e) {
+      this.getOverlayRect();
+      const touch = e.touches[0];
+      const dx = touch.clientX - this.data.centerX;
+      const dy = touch.clientY - this.data.centerY;
+      const startAngle = Math.atan2(dy, dx) * 180 / Math.PI;
+      
+      this.setData({
+        startRotateAngle: startAngle,
+        baseIpRotate: this.data.ipRotate
+      });
+    },
+
+    rotateMove(e) {
+      const touch = e.touches[0];
+      const dx = touch.clientX - this.data.centerX;
+      const dy = touch.clientY - this.data.centerY;
+      const currentAngle = Math.atan2(dy, dx) * 180 / Math.PI;
+      
+      const diff = currentAngle - this.data.startRotateAngle;
+      let nextRotate = this.data.baseIpRotate + diff;
+      
+      this.setData({
+        ipRotate: nextRotate
+      });
+    },
+
+    scaleStart(e) {
+      const touch = e.touches[0];
+      this.setData({
+        startX: touch.clientX,
+        startY: touch.clientY,
+        baseIpScaleX: this.data.ipScaleX,
+        baseIpScaleY: this.data.ipScaleY,
+        scaleOrientation: ''
+      });
+    },
+
+    scaleMove(e) {
+      const touch = e.touches[0];
+      const dx = touch.clientX - this.data.startX;
+      const dy = touch.clientY - this.data.startY;
+      const factor = 0.005;
+      let { scaleOrientation } = this.data;
+
+      if (!scaleOrientation) {
+        const adx = Math.abs(dx);
+        const ady = Math.abs(dy);
+        if (adx > ady && adx > 3) {
+          scaleOrientation = 'x';
+        } else if (ady >= adx && ady > 3) {
+          scaleOrientation = 'y';
+        } else {
+          return;
+        }
+        this.setData({ scaleOrientation });
+      }
+
+      if (scaleOrientation === 'x') {
+        let nextScaleX = this.data.baseIpScaleX + (-dx) * factor;
+        if (nextScaleX < 0.2) nextScaleX = 0.2;
+        if (nextScaleX > 4) nextScaleX = 4;
+        this.setData({ ipScaleX: nextScaleX });
+      } else {
+        let nextScaleY = this.data.baseIpScaleY + (-dy) * factor;
+        if (nextScaleY < 0.2) nextScaleY = 0.2;
+        if (nextScaleY > 4) nextScaleY = 4;
+        this.setData({ ipScaleY: nextScaleY });
+      }
+    },
+    
+    rotateIp() {
+      // 兼容旧的点击事件,如果不需要可以删除,但保留也不会出错
+      if (!this.data.selectedIp) return;
+      const nextRotate = (this.data.ipRotate + 15) % 360;
+      this.setData({
+        ipRotate: nextRotate,
+      });
+    },
+    scaleIp() {
+      if (!this.data.selectedIp) return;
+      let nextScaleX = this.data.ipScaleX + 0.25;
+      let nextScaleY = this.data.ipScaleY + 0.25;
+      if (nextScaleX > 2.5 || nextScaleY > 2.5) {
+        nextScaleX = 1;
+        nextScaleY = 1;
+      }
+      this.setData({
+        ipScaleX: nextScaleX,
+        ipScaleY: nextScaleY,
+      });
+    },
+    deleteIp() {
+      this.setData({
+        selectedIp: null,
+        selectedIpIndex: -1,
+        ipScaleX: 1,
+        ipScaleY: 1,
+        ipRotate: 0,
+        ipConfirmed: false,
+      });
+    },
+    confirmIp() {
+      if (!this.data.selectedIp) return;
+      this.setData({
+        ipConfirmed: true,
+      });
+    },
   },
 });

+ 3 - 0
pages/work/index.json

@@ -1,2 +1,5 @@
 {
+  "usingComponents": {
+    "wxml-to-canvas": "wxml-to-canvas"
+  }
 }

+ 44 - 1
pages/work/index.wxml

@@ -2,12 +2,54 @@
   <image class="work_bg {{!info.filterPath && 'filter'}}" src="{{cdn_url}}/scenes/{{id}}.jpg" mode="aspectFill" />
   <image class="work_bg_filter" wx:if="{{info.filterPath}}" src="{{cdn_url+info.filterPath}}" />
   <view class="w_body">
-    <view class="w_video" style="background-image: url({{cdn_url+info.resourceImg.bg}});{{info.resourceImg.style}}">
+    <view class="top_bar" wx:if="{{projectid == 'ZHS2409020-1'}}">
+      <view class="zoom_tip" wx:if="{{isZoom}}">
+        <image src="/assets/img/img_tip.png" mode="widthFix" />
+        <text style="font-size: 31rpx;">可左右滑动图片</text>
+      </view>
+      <view class="w_zoom" bindtap="zoom">
+        <image src="{{isZoom ? '/assets/img/icon_zoomout.png' : '/assets/img/icon_zoomin.png'}}" mode="widthFix" />
+        <text style="font-size: 31rpx;">{{isZoom ? '全图模式' : '大图模式'}}</text>
+      </view>
+    </view>
+    <view wx:if="{{!isZoom}}" class="w_video" style="background-image: url({{cdn_url+info.resourceImg.bg}});{{info.resourceImg.style}}">
       <video wx:if="{{type=='0'}}" style="opacity:{{loadCompele?1:0}};{{info.resourceImg.imgStyle}}" bindloadedmetadata="loadcompele" src="{{url_link}}" loop autoplay controls="{{false}}" enable-progress-gesture="{{false}}"></video>
       <image wx:else style="opacity:{{loadCompele?1:0}};{{info.resourceImg.imgStyle}}" bindload="loadcompele" show-menu-by-longpress="true" src="{{url_link}}" mode="widthFix" />
+      <view wx:if="{{selectedIp && projectid == 'ZHS2409020-1'}}" class="ip-overlay {{ipConfirmed ? 'ip-overlay__confirmed' : ''}}" style="opacity:{{positionInitialized?1:0}}; {{positionInitialized ? 'left:'+ipLeft+'px; top:'+ipTop+'px; right: auto; bottom: auto;' : ''}}">
+        <image class="ip-main" src="{{selectedIp.imgUrl}}" style="transform:scale({{ipScaleX}}, {{ipScaleY}}) rotate({{ipRotate}}deg) translateZ(0); will-change: transform;" catchtouchstart="dragStart" catchtouchmove="dragMove" />
+        <image class="ip-btn ip-btn__rotate" src="/assets/img/icon_rorate.png" mode="widthFix" catchtouchstart="rotateStart" catchtouchmove="rotateMove" />
+        <image class="ip-btn ip-btn__scale" src="/assets/img/icon_scale.png" mode="widthFix" catchtouchstart="scaleStart" catchtouchmove="scaleMove" />
+        <image class="ip-btn ip-btn__delete" src="/assets/img/icon_delete.png" mode="widthFix" bindtap="deleteIp" />
+        <image class="ip-btn ip-btn__confirm" src="/assets/img/icon_correct.png" mode="widthFix" bindtap="confirmIp" />
+      </view>
     </view>
 
+    <view wx:else class="w_video fill-img">
+      <scroll-view scroll-x="true" class="zoom-scroll" show-scrollbar="{{false}}" bindscroll="onZoomScroll">
+        <image class="zoom-image" mode="heightFix" bindload="loadcompele" show-menu-by-longpress="true" src="{{url_link}}" />
+      </scroll-view>
+      <view wx:if="{{selectedIp && projectid == 'ZHS2409020-1'}}" class="ip-overlay {{ipConfirmed ? 'ip-overlay__confirmed' : ''}}" style="opacity:{{positionInitialized?1:0}}; {{positionInitialized ? 'left:'+ipLeft+'px; top:'+ipTop+'px; right: auto; bottom: auto;' : ''}}">
+        <image class="ip-main" src="{{selectedIp.imgUrl}}" style="transform:scale({{ipScaleX}}, {{ipScaleY}}) rotate({{ipRotate}}deg) translateZ(0); will-change: transform;" catchtouchstart="dragStart" catchtouchmove="dragMove" />
+        <image class="ip-btn ip-btn__rotate" src="/assets/img/icon_rorate.png" mode="widthFix" catchtouchstart="rotateStart" catchtouchmove="rotateMove" />
+        <image class="ip-btn ip-btn__scale" src="/assets/img/icon_scale.png" mode="widthFix" catchtouchstart="scaleStart" catchtouchmove="scaleMove" />
+        <image class="ip-btn ip-btn__delete" src="/assets/img/icon_delete.png" mode="widthFix" bindtap="deleteIp" />
+        <image class="ip-btn ip-btn__confirm" src="/assets/img/icon_correct.png" mode="widthFix" bindtap="confirmIp" />
+      </view>
+    </view>
+    <view wx:if="{{isEditing && projectid == 'ZHS2409020-1'}}" class="ip-list">
+      <scroll-view scroll-y="true" class="ip-scroll" show-scrollbar="{{false}}">
+        <view class="ip-grid">
+          <view class="ip-item-wrapper" wx:for="{{ipsImgList}}" wx:key="name" data-index="{{index}}" bindtap="selectIp">
+            <image class="ip-item {{selectedIpIndex === index ? 'ip-item__active' : ''}}" src="{{item.imgUrl}}" mode="aspectFit" />
+          </view>
+        </view>
+      </scroll-view>
+    </view>
     <view class="w_btn" style="height:{{info.rescan.height}};background:url({{cdn_url+info.rescan.bg}}) no-repeat center / cover">
+      <view style="padding-left: 20rpx;" bindtap="edit" wx:if="{{projectid == 'ZHS2409020-1' && !isEditing}}">
+        <image src="/assets/img/edit.png" mode="widthFix" />
+      </view>
+      <view class="btn_paise__border" wx:if="{{!isEditing}}" style="background:{{info.rescan.borderColor}}" />
       <view bindtap="cancel">
         <image src="{{cdn_url}}/images/cancel.png" mode="widthFix" />
       </view>
@@ -17,4 +59,5 @@
       </view>
     </view>
   </view>
+  <wxml-to-canvas wx:if="{{widgetVisible}}" class="widget" id="widget" width="{{canvasWidth}}" height="{{canvasHeight}}" style="position: absolute; left: -9999px; top: 0;"></wxml-to-canvas>
 </view>

+ 163 - 2
pages/work/index.wxss

@@ -1,5 +1,6 @@
 /* pages/work/index.wxss */
 .w_body {
+  position: relative;
   width: 100%;
   height: 100%;
 }
@@ -25,8 +26,22 @@
 .w_video > video,
 .w_video > image {
   width: 100%;
+  height: 400px;
+}
+.fill-img{
+  width: 100%;
+  height: 600rpx;
+}
+.w_video.fill-img .zoom-scroll {
+  width: 100%;
+  height: 100%;
+  overflow-x: scroll;
+}
+.w_video.fill-img .zoom-image {
+  height: 600rpx;
+  width: auto;
+  display: block;
 }
-
 .work_bg,
 .work_bg_filter {
   position: fixed;
@@ -69,10 +84,156 @@
   justify-content: center;
 }
 .w_btn image {
-  width: 281rpx;
+  width: 240rpx;
 }
 .btn_paise__border {
   width: 4rpx;
   height: 89rpx;
   background: #d9d9d9;
 }
+
+.top_bar {
+  position: fixed;
+  top: 0; /* Adjusted to be below standard header or just visually placed */
+  left: 0;
+  width: 100%;
+  padding: 40rpx;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  z-index: 10001;
+  pointer-events: none;
+}
+
+.zoom_tip {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  pointer-events: auto;
+}
+
+.zoom_tip image {
+  width: 60rpx;
+  height: 60rpx;
+  margin-right: 10rpx;
+  margin-bottom: 0;
+  animation: swipe 1.5s infinite;
+}
+
+.zoom_tip text {
+  color: #fff;
+  font-size: 28rpx;
+  text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.5);
+}
+
+.w_zoom {
+  margin-left: auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 2rpx solid #fff;
+  border-radius: 40rpx;
+  padding: 10rpx 30rpx;
+  color: #fff;
+  font-size: 28rpx;
+  pointer-events: auto;
+}
+
+.w_zoom image {
+  width: 40rpx;
+  height: 40rpx;
+  margin-right: 10rpx;
+}
+
+@keyframes swipe {
+  0% { transform: translateX(-10rpx); }
+  50% { transform: translateX(10rpx); }
+  100% { transform: translateX(-10rpx); }
+}
+
+.ip-list {
+  position: fixed;
+  left: 0;
+  bottom: 140rpx;
+  width: 100%;
+  padding: 20rpx 30rpx;
+  box-sizing: border-box;
+  background: rgba(255, 255, 255, 0.96);
+  z-index: 10002;
+}
+
+.ip-scroll {
+  max-height: 300rpx;
+}
+
+.ip-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.ip-item-wrapper {
+  width: 25%;
+  padding: 10rpx 0;
+  display: flex;
+  justify-content: center;
+}
+
+.ip-item {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 16rpx;
+  background: #f5f5f5;
+}
+
+.ip-item__active {
+  border: 4rpx solid #ff9900;
+  box-sizing: border-box;
+}
+
+.ip-overlay {
+  position: absolute;
+  right: 40rpx;
+  bottom: 40rpx;
+  min-width: 140rpx;
+  max-width: 220rpx;
+  min-height: 160rpx;
+  max-height: 240rpx;
+  z-index: 10001;
+}
+
+.ip-main {
+  min-width: 140rpx;
+  max-width: 220rpx;
+  min-height: 160rpx;
+  max-height: 240rpx;
+}
+
+.ip-btn {
+  position: absolute;
+  width: 84rpx;
+  height: 84rpx;
+}
+
+.ip-btn__rotate {
+  top: -28rpx;
+  left: -28rpx;
+}
+
+.ip-btn__scale {
+  top: -28rpx;
+  right: -28rpx;
+}
+
+.ip-btn__delete {
+  bottom: -28rpx;
+  left: -28rpx;
+}
+
+.ip-btn__confirm {
+  bottom: -28rpx;
+  right: -28rpx;
+}
+
+.ip-overlay__confirmed .ip-btn {
+  display: none;
+}

+ 8 - 3
project.config.json

@@ -38,7 +38,11 @@
     "useStaticServer": true,
     "condition": false,
     "useApiHook": false,
-    "useApiHostProcess": false
+    "useApiHostProcess": false,
+    "compileWorklet": false,
+    "localPlugins": false,
+    "swc": false,
+    "disableSWC": true
   },
   "compileType": "miniprogram",
   "condition": {
@@ -65,10 +69,11 @@
     "tabIndent": "insertSpaces",
     "tabSize": 2
   },
-  "libVersion": "2.19.5",
+  "libVersion": "3.12.1",
   "packOptions": {
     "ignore": [],
     "include": []
   },
-  "appid": "wx5f90253f7065fb77"
+  "appid": "wx5f90253f7065fb77",
+  "simulatorPluginLibVersion": {}
 }

+ 27 - 17
project.private.config.json

@@ -1,34 +1,43 @@
 {
   "setting": {
     "compileHotReLoad": true,
-    "urlCheck": false
+    "urlCheck": false,
+    "coverView": true,
+    "lazyloadPlaceholderEnable": false,
+    "skylineRenderEnable": false,
+    "preloadBackgroundData": false,
+    "autoAudits": false,
+    "useApiHook": false,
+    "showShadowRootInWxmlPanel": true,
+    "useStaticServer": true,
+    "useLanDebug": false,
+    "showES6CompileOption": false,
+    "checkInvalidKey": true,
+    "ignoreDevUnusedFiles": true,
+    "bigPackageSizeSupport": false,
+    "useIsolateContext": false
   },
   "condition": {
     "miniprogram": {
       "list": [
         {
-          "name": "pages/index/index",
-          "pathName": "pages/index/index",
-          "query": "scene=309c23aeaf7a_yW0zeyyxxD9ZDWYa",
-          "launchMode": "default",
-          "scene": null
+          "name": "pages/work/index",
+          "pathName": "pages/work/index",
+          "query": "url_link=123",
+          "scene": null,
+          "launchMode": "default"
         },
         {
-          "name": "pages/example/index",
-          "pathName": "pages/example/index",
-          "query": "id=1",
+          "name": "pages/work/index",
+          "pathName": "pages/work/index",
+          "query": "",
+          "launchMode": "default",
           "scene": null
         },
         {
-          "name": "pages/camera/index",
+          "name": "pages/index/index",
           "pathName": "pages/index/index",
           "query": "",
-          "scene": null
-        },
-        {
-          "name": "pages/work/index",
-          "pathName": "pages/work/index",
-          "query": "type=1&id=1&rdw=kHYlUzoX&projectid=ZHS2305758-1",
           "launchMode": "default",
           "scene": null
         }
@@ -36,5 +45,6 @@
     }
   },
   "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
-  "projectname": "dage-sxk"
+  "projectname": "dage-swk",
+  "libVersion": "3.12.1"
 }