瀏覽代碼

充电详情页优惠券、关联文案修改上线/企业相关完成静态页面

学习?学个屁 2 月之前
父節點
當前提交
e14eb94aa3
共有 100 個文件被更改,包括 6979 次插入536 次删除
  1. 23 4
      App.vue
  2. 735 0
      components/r-canvas/r-canvas.js
  3. 26 0
      components/r-canvas/r-canvas.vue
  4. 1 1
      manifest.json
  5. 28 0
      node_modules/.package-lock.json
  6. 21 0
      node_modules/eventemitter3/LICENSE
  7. 94 0
      node_modules/eventemitter3/README.md
  8. 134 0
      node_modules/eventemitter3/index.d.ts
  9. 336 0
      node_modules/eventemitter3/index.js
  10. 56 0
      node_modules/eventemitter3/package.json
  11. 340 0
      node_modules/eventemitter3/umd/eventemitter3.js
  12. 0 0
      node_modules/eventemitter3/umd/eventemitter3.min.js
  13. 0 0
      node_modules/eventemitter3/umd/eventemitter3.min.js.map
  14. 9 0
      node_modules/widget-ui/babel.config.js
  15. 40 0
      node_modules/widget-ui/dist/element.d.ts
  16. 5 0
      node_modules/widget-ui/dist/event.d.ts
  17. 0 0
      node_modules/widget-ui/dist/index.js
  18. 36 0
      node_modules/widget-ui/dist/style.d.ts
  19. 6 0
      node_modules/widget-ui/jest.config.js
  20. 27 0
      node_modules/widget-ui/package.json
  21. 1186 0
      node_modules/widget-ui/src/css-layout.js
  22. 172 0
      node_modules/widget-ui/src/element.ts
  23. 15 0
      node_modules/widget-ui/src/event.ts
  24. 87 0
      node_modules/widget-ui/src/style.ts
  25. 183 0
      node_modules/widget-ui/test/css-layout.test.ts
  26. 47 0
      node_modules/widget-ui/tsconfig.json
  27. 206 0
      node_modules/widget-ui/tslint.json
  28. 25 0
      node_modules/widget-ui/webpack.config.js
  29. 10 0
      node_modules/wxml-to-canvas/.babelrc
  30. 99 0
      node_modules/wxml-to-canvas/.eslintrc.js
  31. 21 0
      node_modules/wxml-to-canvas/LICENSE
  32. 187 0
      node_modules/wxml-to-canvas/README.md
  33. 26 0
      node_modules/wxml-to-canvas/gulpfile.js
  34. 779 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.js
  35. 4 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.json
  36. 2 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.wxml
  37. 0 0
      node_modules/wxml-to-canvas/miniprogram_dist/index.wxss
  38. 57 0
      node_modules/wxml-to-canvas/miniprogram_dist/utils.js
  39. 63 0
      node_modules/wxml-to-canvas/package.json
  40. 225 0
      node_modules/wxml-to-canvas/src/draw.js
  41. 117 0
      node_modules/wxml-to-canvas/src/index.js
  42. 4 0
      node_modules/wxml-to-canvas/src/index.json
  43. 2 0
      node_modules/wxml-to-canvas/src/index.wxml
  44. 0 0
      node_modules/wxml-to-canvas/src/index.wxss
  45. 57 0
      node_modules/wxml-to-canvas/src/utils.js
  46. 81 0
      node_modules/wxml-to-canvas/src/widget.js
  47. 164 0
      node_modules/wxml-to-canvas/src/xml-parser.js
  48. 31 1
      package-lock.json
  49. 13 0
      package.json
  50. 55 65
      pages.json
  51. 71 0
      pages/Invite-staff/Invite-staff.css
  52. 142 0
      pages/Invite-staff/Invite-staff.vue
  53. 60 41
      pages/coupon-buy/coupon-buy.vue
  54. 149 67
      pages/index/index.css
  55. 12 1
      pages/index/index.vue
  56. 23 15
      pages/login/login.vue
  57. 26 16
      pages/my/my.vue
  58. 36 0
      pages/site-more/site-more.css
  59. 13 1
      pages/site-more/site-more.vue
  60. 32 0
      pages/site/site.css
  61. 13 1
      pages/site/site.vue
  62. 41 1
      pages/terminal/terminal.css
  63. 395 322
      pages/terminal/terminal.vue
  64. 二進制
      static/img/coupon-gr.png
  65. 二進制
      static/img/coupon-group.png
  66. 1 0
      static/img/my-icon06.svg
  67. 二進制
      static/img/share.jpg
  68. 1 0
      static/img/tips-icon.svg
  69. 0 0
      unpackage/dist/build/.automator/mp-weixin/.automator.json
  70. 4 0
      unpackage/dist/build/mp-weixin/app.js
  71. 42 0
      unpackage/dist/build/mp-weixin/app.json
  72. 3 0
      unpackage/dist/build/mp-weixin/app.wxss
  73. 0 0
      unpackage/dist/build/mp-weixin/common/main.js
  74. 0 0
      unpackage/dist/build/mp-weixin/common/main.wxss
  75. 2 0
      unpackage/dist/build/mp-weixin/common/runtime.js
  76. 0 0
      unpackage/dist/build/mp-weixin/common/vendor.js
  77. 0 0
      unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.js
  78. 6 0
      unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.json
  79. 1 0
      unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.wxml
  80. 1 0
      unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.wxss
  81. 10 0
      unpackage/dist/build/mp-weixin/components/ax-body/ax-body.js
  82. 7 0
      unpackage/dist/build/mp-weixin/components/ax-body/ax-body.json
  83. 1 0
      unpackage/dist/build/mp-weixin/components/ax-body/ax-body.wxml
  84. 1 0
      unpackage/dist/build/mp-weixin/components/ax-body/ax-body.wxss
  85. 0 0
      unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.js
  86. 4 0
      unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.json
  87. 1 0
      unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.wxml
  88. 0 0
      unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.wxss
  89. 10 0
      unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.js
  90. 4 0
      unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.json
  91. 1 0
      unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.wxml
  92. 0 0
      unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.wxss
  93. 10 0
      unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.js
  94. 4 0
      unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.json
  95. 1 0
      unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.wxml
  96. 0 0
      unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.wxss
  97. 10 0
      unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.js
  98. 4 0
      unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.json
  99. 1 0
      unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.wxml
  100. 1 0
      unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.wxss

+ 23 - 4
App.vue

@@ -4,8 +4,8 @@
 	export default {
 		onLaunch: function(options) {
 			console.log('App Launch')
-			// 判断用户是否通过分销码进入
-			if (options.scene === 1011) {
+			// 判断用户是否通过分销码进入------------------------------------------------------
+			if (options.scene === 1011|| options.scene === 1012||options.scene === 1013) {
 				const decodedUrl = decodeURIComponent(options.query.q);
 				const urlParts = decodedUrl.split('?');
 				if (urlParts.length > 1) {
@@ -17,11 +17,30 @@
 						resultObj[key] = value;
 					});
 					const adminUserId = resultObj['adminUserId'];
-					uni.setStorageSync('adminUserId', adminUserId);
+					uni.setStorageSync('ADMIN_USERID', adminUserId);
+					console.info(uni.getStorageSync('ADMIN_USERID'),'---用户通过扫码进入')
 				}
 			}
 		},
-		onShow: function() {
+		onShow: function(options) {
+			// 判断用户是否通过分销码进入------------------------------------------------------
+			if (options.scene === 1011||options.scene === 1012||options.scene === 1013) {
+				const decodedUrl = decodeURIComponent(options.query.q);
+				const urlParts = decodedUrl.split('?');
+				if (urlParts.length > 1) {
+					const queryParams = urlParts[1];
+					const paramPairs = queryParams.split('&');
+					const resultObj = {};
+					paramPairs.forEach(pair => {
+						const [key, value] = pair.split('=');
+						resultObj[key] = value;
+					});
+					const adminUserId = resultObj['adminUserId'];
+					uni.setStorageSync('ADMIN_USERID', adminUserId);
+					console.info(uni.getStorageSync('ADMIN_USERID'),'---用户通过扫码进入')
+				}
+			}
+			
 			// 自动更新管理-------------------------------------------------------------------
 			const updateManager = uni.getUpdateManager();
 			updateManager.onUpdateReady(function(res) {

+ 735 - 0
components/r-canvas/r-canvas.js

@@ -0,0 +1,735 @@
+export default{
+	data(){
+		return{
+			system_info:{}, //system info
+			canvas_width:0, //canvas width px
+			canvas_height:0, //canvas height px
+			ctx:null, //canvas object
+			canvas_id:null, //canvas id
+			hidden:true,//Whether to hide canvas
+			scale:1,//canvas scale
+			r_canvas_scale:1,
+			if_ctx:true
+		}
+	},
+	methods:{
+		/**
+		 * save r-canvas.vue object
+		 * @param {Object} that
+		 */
+		// saveThis(that){
+		// 	rCanvasThis = that
+		// },
+		/**
+		 * Draw round rect text
+		 * @param {Object} config
+		 * @param {Number} config.x x坐标
+		 * @param {Number} config.y y坐标
+		 * @param {Number} config.w 宽度
+		 * @param {Number} config.h 高度
+		 * @param {Number} config.radius 圆角弧度
+		 * @param {String} config.fill_color 矩形颜色
+		 */
+		fillRoundRect(config) {
+			return new Promise((resolve,reject)=>{
+				let x = this.compatibilitySize(parseFloat(config.x)*this.scale)
+				let y = this.compatibilitySize(parseFloat(config.y)*this.scale)
+				let w = this.compatibilitySize(parseFloat(config.w)*this.scale)
+				let h = this.compatibilitySize(parseFloat(config.h)*this.scale)
+				let radius = config.radius?parseFloat(config.radius)*this.scale:10*this.scale
+				
+				let fill_color = config.fill_color || "black"
+				// The diameter of the circle must be less than the width and height of the rectangle
+				if (2 * radius > w || 2 * radius > h) { 
+					reject("The diameter of the circle must be less than the width and height of the rectangle")
+					return false; 
+				}
+				this.ctx.save();
+				this.ctx.translate(x, y);
+				//  
+				this.drawRoundRectPath({
+					w: w, 
+					h: h, 
+					radius: radius
+				});
+				this.ctx.fillStyle = fill_color
+				this.ctx.fill();
+				this.ctx.restore();
+				resolve()
+			})
+		},
+		/**
+		 * Draws the sides of a rounded rectangle
+		 * @param {Object} config
+		 * @param {Number} config.w 宽度
+		 * @param {Number} config.h 高度
+		 * @param {Number} config.radius 圆角弧度
+		 */
+		drawRoundRectPath(config) {
+			this.ctx.beginPath(0);
+			this.ctx.arc(config.w - config.radius, config.h - config.radius, config.radius, 0, Math.PI / 2);
+			this.ctx.lineTo(config.radius, config.h);
+			this.ctx.arc(config.radius, config.h - config.radius, config.radius, Math.PI / 2, Math.PI);
+			this.ctx.lineTo(0, config.radius);
+			this.ctx.arc(config.radius, config.radius, config.radius, Math.PI, Math.PI * 3 / 2);
+			this.ctx.lineTo(config.w - config.radius, 0);
+			this.ctx.arc(config.w - config.radius, config.radius, config.radius, Math.PI * 3 / 2, Math.PI * 2);
+			this.ctx.lineTo(config.w, config.h - config.radius);
+			this.ctx.closePath();
+		},
+		/**
+		 * Draw special Text,line wrapping is not supported
+		 * @param {Object} config
+		 * @param {String} config.text 文字
+		 * @param {Number} config.x x坐标
+		 * @param {Number} config.y y坐标
+		 * @param {String} config.font_color 文字颜色
+		 * @param {String} config.font_family 文字字体
+		 * @param {Number} config.font_size 文字大小(px)
+		 */
+		drawSpecialText(params){
+			let general = params.general
+			let list = params.list
+			return new Promise(async (resolve,reject)=>{
+				if(!general){
+					reject("general cannot be empty:101")
+					return;
+				}else if(list && list.length>0){
+					for(let i in list){
+						if(i != 0){
+							let font_size = list[i-1].font_size?parseFloat(list[i-1].font_size):20
+							this.ctx.setFontSize(font_size)
+							general.x = parseFloat(general.x) + this.ctx.measureText(list[i-1].text).width
+						}
+						list[i].x = general.x
+						list[i].y = general.y + (list[i].margin_top?parseFloat(list[i].margin_top):0)
+						await this.drawText(list[i])
+					}
+					resolve()
+				}else{
+					reject("The length of config arr is less than 0")
+					return;
+				}
+				
+			})
+		},
+		/**
+		 * array delete empty
+		 * @param {Object} arr
+		 */
+		arrDeleteEmpty(arr){
+			let newArr = []
+			for(let i in arr){
+				if(arr[i]){
+					newArr.push(arr[i])
+				}
+			}
+			return newArr
+		},
+		/**
+		 * Draw Text,support line
+		 * @param {Object} config
+		 * @param {String} config.text 文字
+		 * @param {Number} config.max_width 文字最大宽度(大于宽度自动换行)
+		 * @param {Number} config.line_height 文字上下行间距
+		 * @param {Number} config.x x坐标
+		 * @param {Number} config.y y坐标
+		 * @param {String} config.font_color 文字颜色
+		 * @param {String} config.font_family 文字字体 默认值:Arial
+		 * @param {String} config.text_align 文字对齐方式(left/center/right)
+		 * @param {Number} config.font_size 文字大小(px)
+		 * @param {Boolean} config.line_through_height 中划线大小
+		 * @param {Boolean} config.line_through_color 中划线颜色
+		 * @param {String} config.font_style 规定文字样式
+		 * @param {String} config.font_variant 规定字体变体
+		 * @param {String} config.font_weight 规定字体粗细
+		 * @param {String} config.line_through_cap 线末端类型
+		 * @param {String} config.line_clamp 最大行数
+		 * @param {String} config.line_clamp_hint 超过line_clamp后,尾部显示的自定义标识 如 ...
+		 * @param {String} config.is_line_break 是否开启换行符换行
+		 * 
+		 */
+		drawText(config,configuration = {}){
+			
+			configuration['line_num'] = configuration.line_num?configuration.line_num:0
+			configuration['text_width'] = configuration.text_width?configuration.text_width:0
+			
+			return new Promise(async (resolve,reject)=>{
+				
+				if(config.text){
+					
+					let draw_width = 0,draw_height = 0,draw_x = config.x,draw_y = config.y
+					let font_size = config.font_size?(parseFloat(config.font_size)*this.scale):(20*this.scale)
+					let font_color = config.font_color || "#000"
+					let font_family = config.font_family || "Arial"
+					let line_height = config.line_height || config.font_size || 20
+					let text_align = config.text_align || "left"
+					let font_weight = config.font_weight || "normal"
+					let font_variant = config.font_variant || "normal"
+					let font_style = config.font_style || "normal"
+					let line_clamp_hint = config.line_clamp_hint || '...'
+					let lineBreakJoinText = ""
+					let max_width = config.max_width?parseFloat(config.max_width)*this.scale:0
+					// checkout is line break
+					if(config.is_line_break){
+						let splitTextArr = config.text.split(/[\n]/g)
+						if(splitTextArr && splitTextArr.length > 0){
+							let newSplitTextArr = this.arrDeleteEmpty(splitTextArr)
+							if(newSplitTextArr && newSplitTextArr.length > 0){
+								lineBreakJoinText = newSplitTextArr.slice(1).join("\n")
+								config.text = newSplitTextArr[0]
+							}else{
+								reject("Text cannot be empty:103")
+								return
+							}
+						}else{
+							reject("Text cannot be empty:102")
+							return
+						}
+					}
+					
+					this.ctx.setFillStyle(font_color) // color
+					this.ctx.textAlign = text_align;
+					this.ctx.font = `${font_style} ${font_variant} ${font_weight} ${parseInt(font_size)}px ${font_family}`
+					if(configuration.text_width >= this.ctx.measureText(config.text).width){
+						draw_width = configuration.text_width
+					}else if(max_width > 0){
+						draw_width = max_width < this.ctx.measureText(config.text).width ? this.resetCompatibilitySize(max_width) : this.resetCompatibilitySize(this.ctx.measureText(config.text).width)
+					}else{
+						draw_width = this.ctx.measureText(config.text).width
+					}
+					configuration.text_width = draw_width / this.scale
+					if( max_width && this.compatibilitySize(this.ctx.measureText(config.text).width) > this.compatibilitySize(max_width)){
+						let current_text = ""
+						let text_arr = config.text.split("")
+						for(let i in text_arr){
+							if( this.compatibilitySize(this.ctx.measureText(current_text+text_arr[i]).width) > this.compatibilitySize(max_width) ){
+								// Hyphenation that is greater than the drawable width continues to draw
+								if(config.line_clamp && parseInt(config.line_clamp) == 1){
+									// Subtracting the current_text tail width from the line_clamp_hint width
+									let current_text_arr = current_text.split('')
+									let json_current_text = ''
+									while(true){
+										current_text_arr = current_text_arr.slice(1)
+										json_current_text = current_text_arr.join('')
+										if(this.compatibilitySize(this.ctx.measureText(json_current_text).width) <= this.compatibilitySize(this.ctx.measureText(line_clamp_hint).width)){
+											current_text = current_text.replace(json_current_text,'')
+											break;
+										}
+									}
+									configuration.line_num += 1
+									this.ctx.setFontSize(parseInt(this.compatibilitySize(font_size))) // font size
+									this.ctx.fillText(current_text + line_clamp_hint, this.compatibilitySize(parseFloat(config.x)*this.scale), this.compatibilitySize(parseFloat(config.y)*this.scale));
+								}else{
+									configuration.line_num += 1
+									this.ctx.setFontSize(parseInt(this.compatibilitySize(font_size))) // font size
+									this.ctx.fillText(current_text, this.compatibilitySize(parseFloat(config.x)*this.scale), this.compatibilitySize(parseFloat(config.y)*this.scale));
+									config.text = text_arr.slice(i).join("")
+									config.y = config.y + line_height
+									if(config.line_clamp){
+										config.line_clamp = parseInt(config.line_clamp) - 1
+									}
+									await this.drawText(config,configuration)
+								}
+								
+								break;
+							}else{
+								current_text = current_text+text_arr[i]
+							}
+						}
+					}else{
+						if(config.line_through_height){
+							let x = parseFloat(config.x)*this.scale
+							let w
+							let y = parseFloat(config.y)*this.scale - (font_size / 2.6) 
+							if(text_align == "left"){
+								w = this.ctx.measureText(config.text).width/1.1 + parseFloat(config.x)*this.scale
+							}else if(text_align == "right"){
+								w = parseFloat(config.x)*this.scale - this.ctx.measureText(config.text).width/1.1
+							}else if(text_align == "center"){
+								x = parseFloat(config.x)*this.scale - this.ctx.measureText(config.text).width / 1.1 / 2
+								w = parseFloat(config.x)*this.scale + this.ctx.measureText(config.text).width / 1.1 / 2
+							}
+							this.drawLineTo({
+								x:x,
+								y:y,
+								w:w,
+								h:y,
+								line_width:config.line_through_height,
+								line_color:config.line_through_color,
+								line_cap:config.line_through_cap
+							})
+						}
+						configuration.line_num += 1
+						this.ctx.setFontSize(parseInt(this.compatibilitySize(font_size))) // font size
+						this.ctx.fillText(config.text, this.compatibilitySize(parseFloat(config.x)*this.scale), this.compatibilitySize(parseFloat(config.y)*this.scale));
+						if(config.line_clamp){
+							config.line_clamp = parseInt(config.line_clamp) - 1
+						}
+					}
+					if(lineBreakJoinText){
+						await this.drawText({...config,text:lineBreakJoinText,y:config.y + line_height},configuration)
+					}
+					draw_height = config.font_size * configuration.line_num
+					draw_width = configuration.text_width
+					resolve({draw_width,draw_height,draw_x,draw_y})
+				}else{
+					reject("Text cannot be empty:101")
+				}
+			})
+		},
+		/**
+		 * Draw Line
+		 * @param {Object} config
+		 * @param {Object} config.x x坐标
+		 * @param {Object} config.y y坐标
+		 * @param {Object} config.w 线的宽度
+		 * @param {Object} config.h 线的高度
+		 * @param {Object} config.line_width 线的宽度
+		 * @param {Object} config.line_color 线条颜色
+		 */
+		drawLineTo(config){
+			let x = this.compatibilitySize(config.x)
+			let y = this.compatibilitySize(config.y)
+			let w = this.compatibilitySize(config.w)
+			let h = this.compatibilitySize(config.h)
+			let line_width = config.line_width?parseFloat(config.line_width)*this.scale:1*this.scale
+			let line_color = config.line_color || "black"
+			let line_cap = config.line_cap || "butt"
+			this.ctx.beginPath()
+			this.ctx.lineCap = line_cap
+			this.ctx.lineWidth = line_width
+			this.ctx.strokeStyle = line_color
+			this.ctx.moveTo(x,y)
+			this.ctx.lineTo(w,h)
+			this.ctx.stroke()
+		},
+		/** 
+		 * Compatibility px
+		 * @param {Object} size
+		 */
+		compatibilitySize(size) {
+		  let canvasSize = (parseFloat(size) / 750) * this.system_info.windowWidth
+		  canvasSize = parseFloat(canvasSize * 2)
+		  return canvasSize
+		},
+		/**
+		 * Restore compatibility px
+		 * @param {Object} size
+		 */
+		resetCompatibilitySize(size) {
+		  let canvasSize = (parseFloat(size/2)/this.system_info.windowWidth) * 750
+		  return canvasSize
+		},
+		/**
+		 * Init canvas
+		 */
+		init(config){
+			return new Promise(async (resolve,reject)=>{
+				if(!config.canvas_id){
+					reject("Canvas ID cannot be empty, please refer to the usage example")
+					return;
+				}
+				this.hidden = config.hidden
+				this.canvas_id = config.canvas_id
+				let system_info = await uni.getSystemInfoSync()
+				this.system_info = system_info
+				this.scale = config.scale&&parseFloat(config.scale)>0?parseInt(config.scale):1
+				this.canvas_width = (config.canvas_width ? this.compatibilitySize(config.canvas_width) : system_info.windowWidth) * this.scale
+				this.canvas_height = (config.canvas_height ? this.compatibilitySize(config.canvas_height) : system_info.windowHeight) * this.scale,
+				this.r_canvas_scale = 1/this.scale
+				this.ctx = uni.createCanvasContext(this.canvas_id,this)
+				this.setCanvasConfig({
+					global_alpha:config.global_alpha?parseFloat(config.global_alpha):1,
+					backgroundColor:config.background_color?config.background_color:"transparent"
+				})
+				resolve()
+			})
+		},
+		/**
+		 * clear canvas all path
+		 */
+		clearCanvas(){
+			return new Promise(async (resolve,reject)=>{
+				if(!this.ctx){
+					reject("canvas is not initialized:101")
+					return
+				}else{
+					this.ctx.clearRect(0,0,parseFloat(this.canvas_width)*this.scale,parseFloat(this.canvas_height)*this.scale)
+					await this.draw()
+					resolve()
+				}
+			})
+		},
+		/**
+		 * Set canvas config
+		 * @param {Object} config
+		 */
+		setCanvasConfig(config){
+			this.ctx.globalAlpha = config.global_alpha
+			this.ctx.fillStyle = config.backgroundColor
+			this.ctx.fillRect(0, 0, parseFloat(this.canvas_width)*this.scale, parseFloat(this.canvas_height)*this.scale)
+		},
+		/**
+		 * set canvas width
+		 * @param {Object} width
+		 */
+		setCanvasWidth(width){
+			if(!width){
+				uni.showToast({
+					title:'setCanvasWidth:width error',
+					icon:'none'
+				})
+			}
+			this.canvas_width = this.compatibilitySize(parseFloat(width)) * this.scale
+			this.ctx.width = this.canvas_width
+		},
+		/**
+		 * set canvas height
+		 * @param {Object} height
+		 */
+		setCanvasHeight(height){
+			if(!height){
+				uni.showToast({
+					title:'setCanvasWidth:height error',
+					icon:'none'
+				})
+			}
+			this.canvas_height = this.compatibilitySize(parseFloat(height)) * this.scale
+			this.ctx.height = this.canvas_height
+		},
+		/**
+		 * Draw to filepath
+		 */
+		draw(callback){
+			return new Promise((resolve,reject)=>{
+				let stop = setTimeout(()=>{
+					this.ctx.draw(false,setTimeout(()=>{
+					    uni.canvasToTempFilePath({
+					    	canvasId: this.canvas_id,
+					    	quality: 1,
+					    	success: (res)=>{
+					    		console.log('res',res)
+					    		resolve(res)
+					    		callback && callback(res)
+					    	},
+					    	fail:(err)=>{
+					    		reject(JSON.stringify(err)|| "Failed to generate poster:101")
+					    	}
+					    },this)
+					},300))
+					clearTimeout(stop)
+				},300)
+			})
+		},
+		/**
+		 * draw rect
+		 * @param {Number} config.x x坐标
+		 * @param {Number} config.y y坐标
+		 * @param {Number} config.w 图形宽度(px)
+		 * @param {Number} config.h 图形高度(px)
+		 * @param {Number} config.color 图形颜色
+		 * @param {Number} config.is_radius 是否开启圆图(1.1.6及以下版本废弃,请使用border_radius)
+		 * @param {Number} config.border_width 边框大小
+		 * @param {Number} config.border_color 边框颜色
+		 * 
+		 */
+		drawRect(config){
+			return new Promise(async (resolve,reject)=>{
+				if(!config.border_width || config.border_width <=0){
+					config.border_width = 0
+				}else{
+					config.border_width = parseFloat(config.border_width)
+				}
+				if(parseFloat(config.border_width) > 0){
+					let sub_config = JSON.parse(JSON.stringify(config))
+					sub_config.border_width = 0
+					sub_config.w = config.w + config.border_width
+					sub_config.h = config.h + config.border_width
+					sub_config.color = config.border_color || 'black'
+					if(sub_config.border_radius){
+						sub_config.border_radius = parseFloat(sub_config.border_radius) + parseFloat(config.border_width) / 2
+					} 
+					await this.drawRect(sub_config)
+				}
+				
+				let color = config.color || 'white'
+				config.x =  (parseFloat(config.x) + config.border_width / 2)
+				config.y = (parseFloat(config.y) + config.border_width / 2)
+				config['color'] = color
+				this.ctx.fillStyle = color;
+				if(config.is_radius || config.border_radius){
+					this.setNativeBorderRadius(config)
+					this.ctx.fill()
+				}else{
+					console.log('config.border_width',config.border_width)
+					this.ctx.fillRect(this.compatibilitySize(config.x*this.scale),this.compatibilitySize(config.y*this.scale),this.compatibilitySize(parseFloat(config.w)*this.scale),this.compatibilitySize(parseFloat(config.h)*this.scale))
+				}
+				resolve()
+			})
+		},
+		/**
+		 * Draw image
+		 * @param {Object} config
+		 * @param {String} config.url 图片链接
+		 * @param {Number} config.x x坐标
+		 * @param {Number} config.y y坐标
+		 * @param {Number} config.w 图片宽度(px)
+		 * @param {Number} config.h 图片高度(px)
+		 * @param {Number} config.border_width 边大小
+		 * @param {Number} config.border_color 边颜色
+		 * @param {Number} config.is_radius 是否开启圆图(1.1.6及以下版本废弃,请使用border_radius)
+		 * @param {Number} config.border_radius 圆角弧度
+		 */
+		drawImage(config){
+			return new Promise(async (resolve,reject)=>{
+				if(config.url){
+					let type = 0 // 1、network image  2、native image  3、base64 image
+					let image_url
+					let reg = /^https?/ig;
+					if(reg.test(config.url)){
+						type = 1
+					}else{
+						if((config.url.indexOf("data:image/png;base64") != -1) || config.url.indexOf("data:image/jpeg;base64") != -1 || config.url.indexOf("data:image/gif;base64") != -1){
+							type = 3
+						}else{
+							type = 2
+						}
+					}
+					if(type == 1){
+						// network image
+						await this.downLoadNetworkFile(config.url).then(res=>{ // two function
+							image_url = res
+						}).catch(err=>{
+							reject(err)
+							return;
+						})
+					}else if(type == 2){
+						// native image
+						const imageInfoResult = await uni.getImageInfo({
+							src: config.url
+						});
+						try{
+							if(imageInfoResult.length <= 1){
+								reject(imageInfoResult[0].errMsg + ':404')
+								return
+							}
+						}catch(e){
+							reject(e+':500')
+							return
+						}
+						let base64 = await this.urlToBase64({url:imageInfoResult[1].path})
+						// #ifdef MP-WEIXIN
+						await this.base64ToNative({url:base64}).then(res=>{
+							image_url = res
+						}).catch(err=>{
+							reject(JSON.stringify(err)+":501")
+							return;
+						})
+						// #endif
+						// #ifndef MP-WEIXIN
+						image_url = base64
+						// #endif
+						
+					}else if(type == 3){
+						// #ifdef MP-WEIXIN
+						await this.base64ToNative({url:config.url}).then(res=>{
+							image_url = res
+						}).catch(err=>{
+							reject(JSON.stringify(err)+":500")
+							return;
+						})
+						// #endif
+						// #ifndef MP-WEIXIN
+						image_url = config.url
+						// #endif
+					}else{
+						reject("Other Type Errors:101")
+						return
+					}
+					if(config.border_width){
+						let border_radius = 0
+						if(config.border_radius){
+							let multiple = config.w / config.border_radius
+							border_radius = (parseFloat(config.w) + parseFloat(config.border_width)) / multiple
+						}
+						// drawRect
+						await this.drawRect({
+							x:parseFloat(config.x) - parseFloat(config.border_width)/2,
+							y:parseFloat(config.y) - parseFloat(config.border_width)/2,
+							w:parseFloat(config.w) + parseFloat(config.border_width),
+							h:parseFloat(config.h) + parseFloat(config.border_width),
+							color:config.border_color,
+							border_radius:border_radius,
+							border_width:config.border_width,
+							is_radius:config.is_radius
+						})
+					}
+					
+					
+
+					if(config.border_radius){
+						config.color =  config.color?config.color:'rgba(0,0,0,0)'
+						
+						// 圆角有白边,+0.5的误差
+						config.w = config.w + 0.3
+						config.h = config.h + 0.3
+						
+						this.setNativeBorderRadius(config)
+					}else if(config.is_radius){
+						//已废弃 is_radius
+						this.ctx.setStrokeStyle("rgba(0,0,0,0)")
+						this.ctx.save()
+						this.ctx.beginPath()
+						this.ctx.arc(this.compatibilitySize(parseFloat(config.x)*this.scale+parseFloat(config.w)*this.scale/2), this.compatibilitySize(parseFloat(config.y)*this.scale+parseFloat(config.h)*this.scale/2), this.compatibilitySize(parseFloat(config.w)*this.scale/2), 0, 2 * Math.PI, false)
+						this.ctx.stroke();
+						this.ctx.clip()
+					}
+					
+					await this.ctx.drawImage(image_url,this.compatibilitySize(parseFloat(config.x)*this.scale),this.compatibilitySize(parseFloat(config.y)*this.scale),this.compatibilitySize(parseFloat(config.w)*this.scale),this.compatibilitySize(parseFloat(config.h)*this.scale))
+					this.ctx.restore() //Restore previously saved drawing context
+					resolve()
+				}else{
+					let err_msg = "Links cannot be empty:101"
+					reject(err_msg)
+				}
+			})
+		},
+		/**
+		 * base64 to native available path
+		 * @param {Object} config
+		 */
+		base64ToNative(config){
+			return new Promise((resolve,reject)=>{
+				let fileName = new Date().getTime()
+				var filePath = `${wx.env.USER_DATA_PATH}/${fileName}_rCanvas.png`
+				wx.getFileSystemManager().writeFile({
+					filePath: filePath,
+					data: config.url.replace(/^data:\S+\/\S+;base64,/, ''),
+					encoding: 'base64',
+					success: function() {
+						resolve(filePath)
+					},
+					fail: function(error) {
+						reject(error)
+					}
+				})
+			})
+		},
+		/**
+		 * native url to base64
+		 * @param {Object} config
+		 */
+		urlToBase64(config){
+			return new Promise(async (resolve,reject)=>{
+				if (typeof window != 'undefined') {
+					await this.downLoadNetworkFile(config.url).then(res=>{ // two function
+						resolve(res)
+					}).catch(err=>{
+						reject(err)
+					})
+				}else if (typeof plus != 'undefined') {
+					plus.io.resolveLocalFileSystemURL(config.url,(obj)=>{
+						obj.file((file)=>{
+							let fileReader = new plus.io.FileReader()
+							fileReader.onload = (res)=>{
+								resolve(res.target.result)
+							}
+							fileReader.onerror = (err)=>{
+								reject(err)
+							}
+							fileReader.readAsDataURL(file)
+						}, (err)=>{
+							reject(err)
+						})
+					},(err)=>{
+						reject(err)
+					})
+				}else if(typeof wx != 'undefined'){
+					wx.getFileSystemManager().readFile({
+						filePath: config.url,
+						encoding: 'base64',
+						success: function(res) {
+							resolve('data:image/png;base64,' + res.data)
+						},
+						fail: function(error) {
+							reject(error)
+						}
+					})
+				}
+			})
+		},
+		setNativeBorderRadius(config){
+			let border_radius = config.border_radius?(parseFloat(config.border_radius)*this.scale):(20*this.scale)
+			if ((parseFloat(config.w)*this.scale) < 2 * border_radius) border_radius = (parseFloat(config.w)*this.scale) / 2;
+			if ((parseFloat(config.h)*this.scale) < 2 * border_radius) border_radius = (parseFloat(config.h)*this.scale) / 2;
+			this.ctx.beginPath();
+			this.ctx.moveTo(this.compatibilitySize((parseFloat(config.x)*this.scale) + border_radius), this.compatibilitySize((parseFloat(config.y)*this.scale)));
+			this.ctx.arcTo(this.compatibilitySize((parseFloat(config.x)*this.scale) + (parseFloat(config.w)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale)), this.compatibilitySize((parseFloat(config.x)*this.scale) + (parseFloat(config.w)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale) + (parseFloat(config.h)*this.scale)), this.compatibilitySize(border_radius));
+			this.ctx.arcTo(this.compatibilitySize((parseFloat(config.x)*this.scale) + (parseFloat(config.w)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale) + (parseFloat(config.h)*this.scale)), this.compatibilitySize((parseFloat(config.x)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale) + (parseFloat(config.h)*this.scale)), this.compatibilitySize(border_radius));
+			this.ctx.arcTo((this.compatibilitySize(parseFloat(config.x)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale) + (parseFloat(config.h)*this.scale)), this.compatibilitySize((parseFloat(config.x)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale)), this.compatibilitySize(border_radius));
+			this.ctx.arcTo(this.compatibilitySize((parseFloat(config.x)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale)), this.compatibilitySize((parseFloat(config.x)*this.scale) + (parseFloat(config.w)*this.scale)), this.compatibilitySize((parseFloat(config.y)*this.scale)), this.compatibilitySize(border_radius));
+			this.ctx.closePath();
+			this.ctx.strokeStyle = config.color || config.border_color || 'rgba(0,0,0,0)'; // 设置绘制边框的颜色
+			this.ctx.stroke();
+			this.ctx.save()
+			this.ctx.clip();
+			
+		},
+		/**
+		 * Download network file
+		 * @param {Object} url : download url
+		 */
+		downLoadNetworkFile(url){
+			return new Promise((resolve,reject)=>{
+				uni.downloadFile({
+					url,
+					success:(res)=>{
+						if(res.statusCode == 200){
+							resolve(res.tempFilePath)
+						}else{
+							reject("Download Image Fail:102")
+						}
+					},
+					fail:(err)=>{
+						reject("Download Image Fail:101")
+					}
+				})
+			})
+		},
+		/**
+		 * Save image to natice
+		 * @param {Object} filePath : native imageUrl
+		 */
+		saveImage(filePath){
+			return new Promise((resolve,reject)=>{
+				if(!filePath){
+					reject("FilePath cannot be null:101")
+					return;
+				}
+				
+				// #ifdef H5
+					var createA = document.createElement("a");
+					createA.download = filePath;
+					createA.href = filePath;
+					document.body.appendChild(createA);
+					createA.click();
+					createA.remove();
+					resolve()
+				// #endif
+				
+				// #ifndef H5
+				uni.saveImageToPhotosAlbum({
+					filePath: filePath,
+					success:(res)=>{
+						resolve(res)
+					}, 
+					fail:(err)=>{
+						reject(err)
+					}
+				})
+				// #endif
+			})
+		}
+	}
+}

+ 26 - 0
components/r-canvas/r-canvas.vue

@@ -0,0 +1,26 @@
+<template>
+	<view>
+		<view class="r-canvas-component" :style="{width:canvas_width/scale+'px',height:canvas_height/scale+'px'}" :class="{'hidden':hidden}">
+			<canvas class="r-canvas" v-if="canvas_id" :canvas-id="canvas_id" :id="canvas_id" :style="{width:canvas_width+'px',height:canvas_height+'px','transform': `scale(${r_canvas_scale})`}"></canvas>
+		</view>
+	</view>
+</template>
+
+<script>
+	import rCanvasJS from "./r-canvas.js"
+	export default {
+		mixins:[rCanvasJS]
+	}
+</script>
+<style>
+.r-canvas{
+	transform-origin: 0 0;
+}
+.r-canvas-component{
+	overflow: hidden;
+}
+.r-canvas-component.hidden{
+	position: fixed;
+	top:-5000upx;
+}
+</style>

+ 1 - 1
manifest.json

@@ -1,6 +1,6 @@
 {
     "name" : "chargingPile",
-    "appid" : "__UNI__DA620DA",
+    "appid" : "__UNI__C0ABCC1",
     "description" : "共享充电桩小程序",
     "versionName" : "1.0.0",
     "versionCode" : "100",

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

@@ -0,0 +1,28 @@
+{
+  "name": "charge_miniapp",
+  "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=="
+    },
+    "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==",
+      "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==",
+      "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.

+ 94 - 0
node_modules/eventemitter3/README.md

@@ -0,0 +1,94 @@
+# EventEmitter3
+
+[![Version npm](https://img.shields.io/npm/v/eventemitter3.svg?style=flat-square)](https://www.npmjs.com/package/eventemitter3)[![Build Status](https://img.shields.io/travis/primus/eventemitter3/master.svg?style=flat-square)](https://travis-ci.org/primus/eventemitter3)[![Dependencies](https://img.shields.io/david/primus/eventemitter3.svg?style=flat-square)](https://david-dm.org/primus/eventemitter3)[![Coverage Status](https://img.shields.io/coveralls/primus/eventemitter3/master.svg?style=flat-square)](https://coveralls.io/r/primus/eventemitter3?branch=master)[![IRC channel](https://img.shields.io/badge/IRC-irc.freenode.net%23primus-00a8ff.svg?style=flat-square)](https://webchat.freenode.net/?channels=primus)
+
+[![Sauce Test Status](https://saucelabs.com/browser-matrix/eventemitter3.svg)](https://saucelabs.com/u/eventemitter3)
+
+EventEmitter3 is a high performance EventEmitter. It has been micro-optimized
+for various of code paths making this, one of, if not the fastest EventEmitter
+available for Node.js and browsers. The module is API compatible with the
+EventEmitter that ships by default with Node.js but there are some slight
+differences:
+
+- Domain support has been removed.
+- We do not `throw` an error when you emit an `error` event and nobody is
+  listening.
+- The `newListener` and `removeListener` events have been removed as they
+  are useful only in some uncommon use-cases.
+- The `setMaxListeners`, `getMaxListeners`, `prependListener` and
+  `prependOnceListener` methods are not available.
+- Support for custom context for events so there is no need to use `fn.bind`.
+- The `removeListener` method removes all matching listeners, not only the
+  first.
+
+It's a drop in replacement for existing EventEmitters, but just faster. Free
+performance, who wouldn't want that? The EventEmitter is written in EcmaScript 3
+so it will work in the oldest browsers and node versions that you need to
+support.
+
+## Installation
+
+```bash
+$ npm install --save eventemitter3
+```
+
+## CDN
+
+Recommended CDN:
+
+```text
+https://unpkg.com/eventemitter3@latest/umd/eventemitter3.min.js
+```
+
+## Usage
+
+After installation the only thing you need to do is require the module:
+
+```js
+var EventEmitter = require('eventemitter3');
+```
+
+And you're ready to create your own EventEmitter instances. For the API
+documentation, please follow the official Node.js documentation:
+
+http://nodejs.org/api/events.html
+
+### Contextual emits
+
+We've upgraded the API of the `EventEmitter.on`, `EventEmitter.once` and
+`EventEmitter.removeListener` to accept an extra argument which is the `context`
+or `this` value that should be set for the emitted events. This means you no
+longer have the overhead of an event that required `fn.bind` in order to get a
+custom `this` value.
+
+```js
+var EE = new EventEmitter()
+  , context = { foo: 'bar' };
+
+function emitted() {
+  console.log(this === context); // true
+}
+
+EE.once('event-name', emitted, context);
+EE.on('another-event', emitted, context);
+EE.removeListener('another-event', emitted, context);
+```
+
+### Tests and benchmarks
+
+This module is well tested. You can run:
+
+- `npm test` to run the tests under Node.js.
+- `npm run test-browser` to run the tests in real browsers via Sauce Labs.
+
+We also have a set of benchmarks to compare EventEmitter3 with some available
+alternatives. To run the benchmarks run `npm run benchmark`.
+
+Tests and benchmarks are not included in the npm package. If you want to play
+with them you have to clone the GitHub repository.
+Note that you will have to run an additional `npm i` in the benchmarks folder
+before `npm run benchmark`.
+
+## License
+
+[MIT](LICENSE)

+ 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"
+  }
+}

+ 340 - 0
node_modules/eventemitter3/umd/eventemitter3.js

@@ -0,0 +1,340 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.EventEmitter3 = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+'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;
+}
+
+},{}]},{},[1])(1)
+});

File diff suppressed because it is too large
+ 0 - 0
node_modules/eventemitter3/umd/eventemitter3.min.js


File diff suppressed because it is too large
+ 0 - 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;
+}

File diff suppressed because it is too large
+ 0 - 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"
+  }
+}

+ 1186 - 0
node_modules/widget-ui/src/css-layout.js

@@ -0,0 +1,1186 @@
+/* eslint-disable */
+// https://www.npmjs.com/package/css-layout
+// change: module export strategy. Use ES6 Module
+
+/**
+ * Copyright (c) 2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+var computeLayout = (function() {
+
+  var CSS_UNDEFINED;
+
+  var CSS_DIRECTION_INHERIT = 'inherit';
+  var CSS_DIRECTION_LTR = 'ltr';
+  var CSS_DIRECTION_RTL = 'rtl';
+
+  var CSS_FLEX_DIRECTION_ROW = 'row';
+  var CSS_FLEX_DIRECTION_ROW_REVERSE = 'row-reverse';
+  var CSS_FLEX_DIRECTION_COLUMN = 'column';
+  var CSS_FLEX_DIRECTION_COLUMN_REVERSE = 'column-reverse';
+
+  var CSS_JUSTIFY_FLEX_START = 'flex-start';
+  var CSS_JUSTIFY_CENTER = 'center';
+  var CSS_JUSTIFY_FLEX_END = 'flex-end';
+  var CSS_JUSTIFY_SPACE_BETWEEN = 'space-between';
+  var CSS_JUSTIFY_SPACE_AROUND = 'space-around';
+
+  var CSS_ALIGN_FLEX_START = 'flex-start';
+  var CSS_ALIGN_CENTER = 'center';
+  var CSS_ALIGN_FLEX_END = 'flex-end';
+  var CSS_ALIGN_STRETCH = 'stretch';
+
+  var CSS_POSITION_RELATIVE = 'relative';
+  var CSS_POSITION_ABSOLUTE = 'absolute';
+
+  var leading = {
+    'row': 'left',
+    'row-reverse': 'right',
+    'column': 'top',
+    'column-reverse': 'bottom'
+  };
+  var trailing = {
+    'row': 'right',
+    'row-reverse': 'left',
+    'column': 'bottom',
+    'column-reverse': 'top'
+  };
+  var pos = {
+    'row': 'left',
+    'row-reverse': 'right',
+    'column': 'top',
+    'column-reverse': 'bottom'
+  };
+  var dim = {
+    'row': 'width',
+    'row-reverse': 'width',
+    'column': 'height',
+    'column-reverse': 'height'
+  };
+
+  // When transpiled to Java / C the node type has layout, children and style
+  // properties. For the JavaScript version this function adds these properties
+  // if they don't already exist.
+  function fillNodes(node) {
+    if (!node.layout || node.isDirty) {
+      node.layout = {
+        width: undefined,
+        height: undefined,
+        top: 0,
+        left: 0,
+        right: 0,
+        bottom: 0
+      };
+    }
+
+    if (!node.style) {
+      node.style = {};
+    }
+
+    if (!node.children) {
+      node.children = [];
+    }
+    node.children.forEach(fillNodes);
+    return node;
+  }
+
+  function isUndefined(value) {
+    return value === undefined;
+  }
+
+  function isRowDirection(flexDirection) {
+    return flexDirection === CSS_FLEX_DIRECTION_ROW ||
+           flexDirection === CSS_FLEX_DIRECTION_ROW_REVERSE;
+  }
+
+  function isColumnDirection(flexDirection) {
+    return flexDirection === CSS_FLEX_DIRECTION_COLUMN ||
+           flexDirection === CSS_FLEX_DIRECTION_COLUMN_REVERSE;
+  }
+
+  function getLeadingMargin(node, axis) {
+    if (node.style.marginStart !== undefined && isRowDirection(axis)) {
+      return node.style.marginStart;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.marginLeft;   break;
+      case 'row-reverse':    value = node.style.marginRight;  break;
+      case 'column':         value = node.style.marginTop;    break;
+      case 'column-reverse': value = node.style.marginBottom; break;
+    }
+
+    if (value !== undefined) {
+      return value;
+    }
+
+    if (node.style.margin !== undefined) {
+      return node.style.margin;
+    }
+
+    return 0;
+  }
+
+  function getTrailingMargin(node, axis) {
+    if (node.style.marginEnd !== undefined && isRowDirection(axis)) {
+      return node.style.marginEnd;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.marginRight;  break;
+      case 'row-reverse':    value = node.style.marginLeft;   break;
+      case 'column':         value = node.style.marginBottom; break;
+      case 'column-reverse': value = node.style.marginTop;    break;
+    }
+
+    if (value != null) {
+      return value;
+    }
+
+    if (node.style.margin !== undefined) {
+      return node.style.margin;
+    }
+
+    return 0;
+  }
+
+  function getLeadingPadding(node, axis) {
+    if (node.style.paddingStart !== undefined && node.style.paddingStart >= 0
+        && isRowDirection(axis)) {
+      return node.style.paddingStart;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.paddingLeft;   break;
+      case 'row-reverse':    value = node.style.paddingRight;  break;
+      case 'column':         value = node.style.paddingTop;    break;
+      case 'column-reverse': value = node.style.paddingBottom; break;
+    }
+
+    if (value != null && value >= 0) {
+      return value;
+    }
+
+    if (node.style.padding !== undefined && node.style.padding >= 0) {
+      return node.style.padding;
+    }
+
+    return 0;
+  }
+
+  function getTrailingPadding(node, axis) {
+    if (node.style.paddingEnd !== undefined && node.style.paddingEnd >= 0
+        && isRowDirection(axis)) {
+      return node.style.paddingEnd;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.paddingRight;  break;
+      case 'row-reverse':    value = node.style.paddingLeft;   break;
+      case 'column':         value = node.style.paddingBottom; break;
+      case 'column-reverse': value = node.style.paddingTop;    break;
+    }
+
+    if (value != null && value >= 0) {
+      return value;
+    }
+
+    if (node.style.padding !== undefined && node.style.padding >= 0) {
+      return node.style.padding;
+    }
+
+    return 0;
+  }
+
+  function getLeadingBorder(node, axis) {
+    if (node.style.borderStartWidth !== undefined && node.style.borderStartWidth >= 0
+        && isRowDirection(axis)) {
+      return node.style.borderStartWidth;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.borderLeftWidth;   break;
+      case 'row-reverse':    value = node.style.borderRightWidth;  break;
+      case 'column':         value = node.style.borderTopWidth;    break;
+      case 'column-reverse': value = node.style.borderBottomWidth; break;
+    }
+
+    if (value != null && value >= 0) {
+      return value;
+    }
+
+    if (node.style.borderWidth !== undefined && node.style.borderWidth >= 0) {
+      return node.style.borderWidth;
+    }
+
+    return 0;
+  }
+
+  function getTrailingBorder(node, axis) {
+    if (node.style.borderEndWidth !== undefined && node.style.borderEndWidth >= 0
+        && isRowDirection(axis)) {
+      return node.style.borderEndWidth;
+    }
+
+    var value = null;
+    switch (axis) {
+      case 'row':            value = node.style.borderRightWidth;  break;
+      case 'row-reverse':    value = node.style.borderLeftWidth;   break;
+      case 'column':         value = node.style.borderBottomWidth; break;
+      case 'column-reverse': value = node.style.borderTopWidth;    break;
+    }
+
+    if (value != null && value >= 0) {
+      return value;
+    }
+
+    if (node.style.borderWidth !== undefined && node.style.borderWidth >= 0) {
+      return node.style.borderWidth;
+    }
+
+    return 0;
+  }
+
+  function getLeadingPaddingAndBorder(node, axis) {
+    return getLeadingPadding(node, axis) + getLeadingBorder(node, axis);
+  }
+
+  function getTrailingPaddingAndBorder(node, axis) {
+    return getTrailingPadding(node, axis) + getTrailingBorder(node, axis);
+  }
+
+  function getBorderAxis(node, axis) {
+    return getLeadingBorder(node, axis) + getTrailingBorder(node, axis);
+  }
+
+  function getMarginAxis(node, axis) {
+    return getLeadingMargin(node, axis) + getTrailingMargin(node, axis);
+  }
+
+  function getPaddingAndBorderAxis(node, axis) {
+    return getLeadingPaddingAndBorder(node, axis) +
+        getTrailingPaddingAndBorder(node, axis);
+  }
+
+  function getJustifyContent(node) {
+    if (node.style.justifyContent) {
+      return node.style.justifyContent;
+    }
+    return 'flex-start';
+  }
+
+  function getAlignContent(node) {
+    if (node.style.alignContent) {
+      return node.style.alignContent;
+    }
+    return 'flex-start';
+  }
+
+  function getAlignItem(node, child) {
+    if (child.style.alignSelf) {
+      return child.style.alignSelf;
+    }
+    if (node.style.alignItems) {
+      return node.style.alignItems;
+    }
+    return 'stretch';
+  }
+
+  function resolveAxis(axis, direction) {
+    if (direction === CSS_DIRECTION_RTL) {
+      if (axis === CSS_FLEX_DIRECTION_ROW) {
+        return CSS_FLEX_DIRECTION_ROW_REVERSE;
+      } else if (axis === CSS_FLEX_DIRECTION_ROW_REVERSE) {
+        return CSS_FLEX_DIRECTION_ROW;
+      }
+    }
+
+    return axis;
+  }
+
+  function resolveDirection(node, parentDirection) {
+    var direction;
+    if (node.style.direction) {
+      direction = node.style.direction;
+    } else {
+      direction = CSS_DIRECTION_INHERIT;
+    }
+
+    if (direction === CSS_DIRECTION_INHERIT) {
+      direction = (parentDirection === undefined ? CSS_DIRECTION_LTR : parentDirection);
+    }
+
+    return direction;
+  }
+
+  function getFlexDirection(node) {
+    if (node.style.flexDirection) {
+      return node.style.flexDirection;
+    }
+    return CSS_FLEX_DIRECTION_COLUMN;
+  }
+
+  function getCrossFlexDirection(flexDirection, direction) {
+    if (isColumnDirection(flexDirection)) {
+      return resolveAxis(CSS_FLEX_DIRECTION_ROW, direction);
+    } else {
+      return CSS_FLEX_DIRECTION_COLUMN;
+    }
+  }
+
+  function getPositionType(node) {
+    if (node.style.position) {
+      return node.style.position;
+    }
+    return 'relative';
+  }
+
+  function isFlex(node) {
+    return (
+      getPositionType(node) === CSS_POSITION_RELATIVE &&
+      node.style.flex > 0
+    );
+  }
+
+  function isFlexWrap(node) {
+    return node.style.flexWrap === 'wrap';
+  }
+
+  function getDimWithMargin(node, axis) {
+    return node.layout[dim[axis]] + getMarginAxis(node, axis);
+  }
+
+  function isDimDefined(node, axis) {
+    return node.style[dim[axis]] !== undefined && node.style[dim[axis]] >= 0;
+  }
+
+  function isPosDefined(node, pos) {
+    return node.style[pos] !== undefined;
+  }
+
+  function isMeasureDefined(node) {
+    return node.style.measure !== undefined;
+  }
+
+  function getPosition(node, pos) {
+    if (node.style[pos] !== undefined) {
+      return node.style[pos];
+    }
+    return 0;
+  }
+
+  function boundAxis(node, axis, value) {
+    var min = {
+      'row': node.style.minWidth,
+      'row-reverse': node.style.minWidth,
+      'column': node.style.minHeight,
+      'column-reverse': node.style.minHeight
+    }[axis];
+
+    var max = {
+      'row': node.style.maxWidth,
+      'row-reverse': node.style.maxWidth,
+      'column': node.style.maxHeight,
+      'column-reverse': node.style.maxHeight
+    }[axis];
+
+    var boundValue = value;
+    if (max !== undefined && max >= 0 && boundValue > max) {
+      boundValue = max;
+    }
+    if (min !== undefined && min >= 0 && boundValue < min) {
+      boundValue = min;
+    }
+    return boundValue;
+  }
+
+  function fmaxf(a, b) {
+    if (a > b) {
+      return a;
+    }
+    return b;
+  }
+
+  // When the user specifically sets a value for width or height
+  function setDimensionFromStyle(node, axis) {
+    // The parent already computed us a width or height. We just skip it
+    if (node.layout[dim[axis]] !== undefined) {
+      return;
+    }
+    // We only run if there's a width or height defined
+    if (!isDimDefined(node, axis)) {
+      return;
+    }
+
+    // The dimensions can never be smaller than the padding and border
+    node.layout[dim[axis]] = fmaxf(
+      boundAxis(node, axis, node.style[dim[axis]]),
+      getPaddingAndBorderAxis(node, axis)
+    );
+  }
+
+  function setTrailingPosition(node, child, axis) {
+    child.layout[trailing[axis]] = node.layout[dim[axis]] -
+        child.layout[dim[axis]] - child.layout[pos[axis]];
+  }
+
+  // If both left and right are defined, then use left. Otherwise return
+  // +left or -right depending on which is defined.
+  function getRelativePosition(node, axis) {
+    if (node.style[leading[axis]] !== undefined) {
+      return getPosition(node, leading[axis]);
+    }
+    return -getPosition(node, trailing[axis]);
+  }
+
+  function layoutNodeImpl(node, parentMaxWidth, /*css_direction_t*/parentDirection) {
+    var/*css_direction_t*/ direction = resolveDirection(node, parentDirection);
+    var/*(c)!css_flex_direction_t*//*(java)!int*/ mainAxis = resolveAxis(getFlexDirection(node), direction);
+    var/*(c)!css_flex_direction_t*//*(java)!int*/ crossAxis = getCrossFlexDirection(mainAxis, direction);
+    var/*(c)!css_flex_direction_t*//*(java)!int*/ resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction);
+
+    // Handle width and height style attributes
+    setDimensionFromStyle(node, mainAxis);
+    setDimensionFromStyle(node, crossAxis);
+
+    // Set the resolved resolution in the node's layout
+    node.layout.direction = direction;
+
+    // The position is set by the parent, but we need to complete it with a
+    // delta composed of the margin and left/top/right/bottom
+    node.layout[leading[mainAxis]] += getLeadingMargin(node, mainAxis) +
+      getRelativePosition(node, mainAxis);
+    node.layout[trailing[mainAxis]] += getTrailingMargin(node, mainAxis) +
+      getRelativePosition(node, mainAxis);
+    node.layout[leading[crossAxis]] += getLeadingMargin(node, crossAxis) +
+      getRelativePosition(node, crossAxis);
+    node.layout[trailing[crossAxis]] += getTrailingMargin(node, crossAxis) +
+      getRelativePosition(node, crossAxis);
+
+    // Inline immutable values from the target node to avoid excessive method
+    // invocations during the layout calculation.
+    var/*int*/ childCount = node.children.length;
+    var/*float*/ paddingAndBorderAxisResolvedRow = getPaddingAndBorderAxis(node, resolvedRowAxis);
+
+    if (isMeasureDefined(node)) {
+      var/*bool*/ isResolvedRowDimDefined = !isUndefined(node.layout[dim[resolvedRowAxis]]);
+
+      var/*float*/ width = CSS_UNDEFINED;
+      if (isDimDefined(node, resolvedRowAxis)) {
+        width = node.style.width;
+      } else if (isResolvedRowDimDefined) {
+        width = node.layout[dim[resolvedRowAxis]];
+      } else {
+        width = parentMaxWidth -
+          getMarginAxis(node, resolvedRowAxis);
+      }
+      width -= paddingAndBorderAxisResolvedRow;
+
+      // We only need to give a dimension for the text if we haven't got any
+      // for it computed yet. It can either be from the style attribute or because
+      // the element is flexible.
+      var/*bool*/ isRowUndefined = !isDimDefined(node, resolvedRowAxis) && !isResolvedRowDimDefined;
+      var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) &&
+        isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]);
+
+      // Let's not measure the text if we already know both dimensions
+      if (isRowUndefined || isColumnUndefined) {
+        var/*css_dim_t*/ measureDim = node.style.measure(
+          /*(c)!node->context,*/
+          /*(java)!layoutContext.measureOutput,*/
+          width
+        );
+        if (isRowUndefined) {
+          node.layout.width = measureDim.width +
+            paddingAndBorderAxisResolvedRow;
+        }
+        if (isColumnUndefined) {
+          node.layout.height = measureDim.height +
+            getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN);
+        }
+      }
+      if (childCount === 0) {
+        return;
+      }
+    }
+
+    var/*bool*/ isNodeFlexWrap = isFlexWrap(node);
+
+    var/*css_justify_t*/ justifyContent = getJustifyContent(node);
+
+    var/*float*/ leadingPaddingAndBorderMain = getLeadingPaddingAndBorder(node, mainAxis);
+    var/*float*/ leadingPaddingAndBorderCross = getLeadingPaddingAndBorder(node, crossAxis);
+    var/*float*/ paddingAndBorderAxisMain = getPaddingAndBorderAxis(node, mainAxis);
+    var/*float*/ paddingAndBorderAxisCross = getPaddingAndBorderAxis(node, crossAxis);
+
+    var/*bool*/ isMainDimDefined = !isUndefined(node.layout[dim[mainAxis]]);
+    var/*bool*/ isCrossDimDefined = !isUndefined(node.layout[dim[crossAxis]]);
+    var/*bool*/ isMainRowDirection = isRowDirection(mainAxis);
+
+    var/*int*/ i;
+    var/*int*/ ii;
+    var/*css_node_t**/ child;
+    var/*(c)!css_flex_direction_t*//*(java)!int*/ axis;
+
+    var/*css_node_t**/ firstAbsoluteChild = null;
+    var/*css_node_t**/ currentAbsoluteChild = null;
+
+    var/*float*/ definedMainDim = CSS_UNDEFINED;
+    if (isMainDimDefined) {
+      definedMainDim = node.layout[dim[mainAxis]] - paddingAndBorderAxisMain;
+    }
+
+    // We want to execute the next two loops one per line with flex-wrap
+    var/*int*/ startLine = 0;
+    var/*int*/ endLine = 0;
+    // var/*int*/ nextOffset = 0;
+    var/*int*/ alreadyComputedNextLayout = 0;
+    // We aggregate the total dimensions of the container in those two variables
+    var/*float*/ linesCrossDim = 0;
+    var/*float*/ linesMainDim = 0;
+    var/*int*/ linesCount = 0;
+    while (endLine < childCount) {
+      // <Loop A> Layout non flexible children and count children by type
+
+      // mainContentDim is accumulation of the dimensions and margin of all the
+      // non flexible children. This will be used in order to either set the
+      // dimensions of the node if none already exist, or to compute the
+      // remaining space left for the flexible children.
+      var/*float*/ mainContentDim = 0;
+
+      // There are three kind of children, non flexible, flexible and absolute.
+      // We need to know how many there are in order to distribute the space.
+      var/*int*/ flexibleChildrenCount = 0;
+      var/*float*/ totalFlexible = 0;
+      var/*int*/ nonFlexibleChildrenCount = 0;
+
+      // Use the line loop to position children in the main axis for as long
+      // as they are using a simple stacking behaviour. Children that are
+      // immediately stacked in the initial loop will not be touched again
+      // in <Loop C>.
+      var/*bool*/ isSimpleStackMain =
+          (isMainDimDefined && justifyContent === CSS_JUSTIFY_FLEX_START) ||
+          (!isMainDimDefined && justifyContent !== CSS_JUSTIFY_CENTER);
+      var/*int*/ firstComplexMain = (isSimpleStackMain ? childCount : startLine);
+
+      // Use the initial line loop to position children in the cross axis for
+      // as long as they are relatively positioned with alignment STRETCH or
+      // FLEX_START. Children that are immediately stacked in the initial loop
+      // will not be touched again in <Loop D>.
+      var/*bool*/ isSimpleStackCross = true;
+      var/*int*/ firstComplexCross = childCount;
+
+      var/*css_node_t**/ firstFlexChild = null;
+      var/*css_node_t**/ currentFlexChild = null;
+
+      var/*float*/ mainDim = leadingPaddingAndBorderMain;
+      var/*float*/ crossDim = 0;
+
+      var/*float*/ maxWidth;
+      for (i = startLine; i < childCount; ++i) {
+        child = node.children[i];
+        child.lineIndex = linesCount;
+
+        child.nextAbsoluteChild = null;
+        child.nextFlexChild = null;
+
+        var/*css_align_t*/ alignItem = getAlignItem(node, child);
+
+        // Pre-fill cross axis dimensions when the child is using stretch before
+        // we call the recursive layout pass
+        if (alignItem === CSS_ALIGN_STRETCH &&
+            getPositionType(child) === CSS_POSITION_RELATIVE &&
+            isCrossDimDefined &&
+            !isDimDefined(child, crossAxis)) {
+          child.layout[dim[crossAxis]] = fmaxf(
+            boundAxis(child, crossAxis, node.layout[dim[crossAxis]] -
+              paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)),
+            // You never want to go smaller than padding
+            getPaddingAndBorderAxis(child, crossAxis)
+          );
+        } else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) {
+          // Store a private linked list of absolutely positioned children
+          // so that we can efficiently traverse them later.
+          if (firstAbsoluteChild === null) {
+            firstAbsoluteChild = child;
+          }
+          if (currentAbsoluteChild !== null) {
+            currentAbsoluteChild.nextAbsoluteChild = child;
+          }
+          currentAbsoluteChild = child;
+
+          // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
+          // left and right or top and bottom).
+          for (ii = 0; ii < 2; ii++) {
+            axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
+            if (!isUndefined(node.layout[dim[axis]]) &&
+                !isDimDefined(child, axis) &&
+                isPosDefined(child, leading[axis]) &&
+                isPosDefined(child, trailing[axis])) {
+              child.layout[dim[axis]] = fmaxf(
+                boundAxis(child, axis, node.layout[dim[axis]] -
+                  getPaddingAndBorderAxis(node, axis) -
+                  getMarginAxis(child, axis) -
+                  getPosition(child, leading[axis]) -
+                  getPosition(child, trailing[axis])),
+                // You never want to go smaller than padding
+                getPaddingAndBorderAxis(child, axis)
+              );
+            }
+          }
+        }
+
+        var/*float*/ nextContentDim = 0;
+
+        // It only makes sense to consider a child flexible if we have a computed
+        // dimension for the node.
+        if (isMainDimDefined && isFlex(child)) {
+          flexibleChildrenCount++;
+          totalFlexible += child.style.flex;
+
+          // Store a private linked list of flexible children so that we can
+          // efficiently traverse them later.
+          if (firstFlexChild === null) {
+            firstFlexChild = child;
+          }
+          if (currentFlexChild !== null) {
+            currentFlexChild.nextFlexChild = child;
+          }
+          currentFlexChild = child;
+
+          // Even if we don't know its exact size yet, we already know the padding,
+          // border and margin. We'll use this partial information, which represents
+          // the smallest possible size for the child, to compute the remaining
+          // available space.
+          nextContentDim = getPaddingAndBorderAxis(child, mainAxis) +
+            getMarginAxis(child, mainAxis);
+
+        } else {
+          maxWidth = CSS_UNDEFINED;
+          if (!isMainRowDirection) {
+            if (isDimDefined(node, resolvedRowAxis)) {
+              maxWidth = node.layout[dim[resolvedRowAxis]] -
+                paddingAndBorderAxisResolvedRow;
+            } else {
+              maxWidth = parentMaxWidth -
+                getMarginAxis(node, resolvedRowAxis) -
+                paddingAndBorderAxisResolvedRow;
+            }
+          }
+
+          // This is the main recursive call. We layout non flexible children.
+          if (alreadyComputedNextLayout === 0) {
+            layoutNode(/*(java)!layoutContext, */child, maxWidth, direction);
+          }
+
+          // Absolute positioned elements do not take part of the layout, so we
+          // don't use them to compute mainContentDim
+          if (getPositionType(child) === CSS_POSITION_RELATIVE) {
+            nonFlexibleChildrenCount++;
+            // At this point we know the final size and margin of the element.
+            nextContentDim = getDimWithMargin(child, mainAxis);
+          }
+        }
+
+        // The element we are about to add would make us go to the next line
+        if (isNodeFlexWrap &&
+            isMainDimDefined &&
+            mainContentDim + nextContentDim > definedMainDim &&
+            // If there's only one element, then it's bigger than the content
+            // and needs its own line
+            i !== startLine) {
+          nonFlexibleChildrenCount--;
+          alreadyComputedNextLayout = 1;
+          break;
+        }
+
+        // Disable simple stacking in the main axis for the current line as
+        // we found a non-trivial child. The remaining children will be laid out
+        // in <Loop C>.
+        if (isSimpleStackMain &&
+            (getPositionType(child) !== CSS_POSITION_RELATIVE || isFlex(child))) {
+          isSimpleStackMain = false;
+          firstComplexMain = i;
+        }
+
+        // Disable simple stacking in the cross axis for the current line as
+        // we found a non-trivial child. The remaining children will be laid out
+        // in <Loop D>.
+        if (isSimpleStackCross &&
+            (getPositionType(child) !== CSS_POSITION_RELATIVE ||
+                (alignItem !== CSS_ALIGN_STRETCH && alignItem !== CSS_ALIGN_FLEX_START) ||
+                isUndefined(child.layout[dim[crossAxis]]))) {
+          isSimpleStackCross = false;
+          firstComplexCross = i;
+        }
+
+        if (isSimpleStackMain) {
+          child.layout[pos[mainAxis]] += mainDim;
+          if (isMainDimDefined) {
+            setTrailingPosition(node, child, mainAxis);
+          }
+
+          mainDim += getDimWithMargin(child, mainAxis);
+          crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis)));
+        }
+
+        if (isSimpleStackCross) {
+          child.layout[pos[crossAxis]] += linesCrossDim + leadingPaddingAndBorderCross;
+          if (isCrossDimDefined) {
+            setTrailingPosition(node, child, crossAxis);
+          }
+        }
+
+        alreadyComputedNextLayout = 0;
+        mainContentDim += nextContentDim;
+        endLine = i + 1;
+      }
+
+      // <Loop B> Layout flexible children and allocate empty space
+
+      // In order to position the elements in the main axis, we have two
+      // controls. The space between the beginning and the first element
+      // and the space between each two elements.
+      var/*float*/ leadingMainDim = 0;
+      var/*float*/ betweenMainDim = 0;
+
+      // The remaining available space that needs to be allocated
+      var/*float*/ remainingMainDim = 0;
+      if (isMainDimDefined) {
+        remainingMainDim = definedMainDim - mainContentDim;
+      } else {
+        remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim;
+      }
+
+      // If there are flexible children in the mix, they are going to fill the
+      // remaining space
+      if (flexibleChildrenCount !== 0) {
+        var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible;
+        var/*float*/ baseMainDim;
+        var/*float*/ boundMainDim;
+
+        // If the flex share of remaining space doesn't meet min/max bounds,
+        // remove this child from flex calculations.
+        currentFlexChild = firstFlexChild;
+        while (currentFlexChild !== null) {
+          baseMainDim = flexibleMainDim * currentFlexChild.style.flex +
+              getPaddingAndBorderAxis(currentFlexChild, mainAxis);
+          boundMainDim = boundAxis(currentFlexChild, mainAxis, baseMainDim);
+
+          if (baseMainDim !== boundMainDim) {
+            remainingMainDim -= boundMainDim;
+            totalFlexible -= currentFlexChild.style.flex;
+          }
+
+          currentFlexChild = currentFlexChild.nextFlexChild;
+        }
+        flexibleMainDim = remainingMainDim / totalFlexible;
+
+        // The non flexible children can overflow the container, in this case
+        // we should just assume that there is no space available.
+        if (flexibleMainDim < 0) {
+          flexibleMainDim = 0;
+        }
+
+        currentFlexChild = firstFlexChild;
+        while (currentFlexChild !== null) {
+          // At this point we know the final size of the element in the main
+          // dimension
+          currentFlexChild.layout[dim[mainAxis]] = boundAxis(currentFlexChild, mainAxis,
+            flexibleMainDim * currentFlexChild.style.flex +
+                getPaddingAndBorderAxis(currentFlexChild, mainAxis)
+          );
+
+          maxWidth = CSS_UNDEFINED;
+          if (isDimDefined(node, resolvedRowAxis)) {
+            maxWidth = node.layout[dim[resolvedRowAxis]] -
+              paddingAndBorderAxisResolvedRow;
+          } else if (!isMainRowDirection) {
+            maxWidth = parentMaxWidth -
+              getMarginAxis(node, resolvedRowAxis) -
+              paddingAndBorderAxisResolvedRow;
+          }
+
+          // And we recursively call the layout algorithm for this child
+          layoutNode(/*(java)!layoutContext, */currentFlexChild, maxWidth, direction);
+
+          child = currentFlexChild;
+          currentFlexChild = currentFlexChild.nextFlexChild;
+          child.nextFlexChild = null;
+        }
+
+      // We use justifyContent to figure out how to allocate the remaining
+      // space available
+      } else if (justifyContent !== CSS_JUSTIFY_FLEX_START) {
+        if (justifyContent === CSS_JUSTIFY_CENTER) {
+          leadingMainDim = remainingMainDim / 2;
+        } else if (justifyContent === CSS_JUSTIFY_FLEX_END) {
+          leadingMainDim = remainingMainDim;
+        } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) {
+          remainingMainDim = fmaxf(remainingMainDim, 0);
+          if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) {
+            betweenMainDim = remainingMainDim /
+              (flexibleChildrenCount + nonFlexibleChildrenCount - 1);
+          } else {
+            betweenMainDim = 0;
+          }
+        } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) {
+          // Space on the edges is half of the space between elements
+          betweenMainDim = remainingMainDim /
+            (flexibleChildrenCount + nonFlexibleChildrenCount);
+          leadingMainDim = betweenMainDim / 2;
+        }
+      }
+
+      // <Loop C> Position elements in the main axis and compute dimensions
+
+      // At this point, all the children have their dimensions set. We need to
+      // find their position. In order to do that, we accumulate data in
+      // variables that are also useful to compute the total dimensions of the
+      // container!
+      mainDim += leadingMainDim;
+
+      for (i = firstComplexMain; i < endLine; ++i) {
+        child = node.children[i];
+
+        if (getPositionType(child) === CSS_POSITION_ABSOLUTE &&
+            isPosDefined(child, leading[mainAxis])) {
+          // In case the child is position absolute and has left/top being
+          // defined, we override the position to whatever the user said
+          // (and margin/border).
+          child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) +
+            getLeadingBorder(node, mainAxis) +
+            getLeadingMargin(child, mainAxis);
+        } else {
+          // If the child is position absolute (without top/left) or relative,
+          // we put it at the current accumulated offset.
+          child.layout[pos[mainAxis]] += mainDim;
+
+          // Define the trailing position accordingly.
+          if (isMainDimDefined) {
+            setTrailingPosition(node, child, mainAxis);
+          }
+
+          // Now that we placed the element, we need to update the variables
+          // We only need to do that for relative elements. Absolute elements
+          // do not take part in that phase.
+          if (getPositionType(child) === CSS_POSITION_RELATIVE) {
+            // The main dimension is the sum of all the elements dimension plus
+            // the spacing.
+            mainDim += betweenMainDim + getDimWithMargin(child, mainAxis);
+            // The cross dimension is the max of the elements dimension since there
+            // can only be one element in that cross dimension.
+            crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis)));
+          }
+        }
+      }
+
+      var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]];
+      if (!isCrossDimDefined) {
+        containerCrossAxis = fmaxf(
+          // For the cross dim, we add both sides at the end because the value
+          // is aggregate via a max function. Intermediate negative values
+          // can mess this computation otherwise
+          boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross),
+          paddingAndBorderAxisCross
+        );
+      }
+
+      // <Loop D> Position elements in the cross axis
+      for (i = firstComplexCross; i < endLine; ++i) {
+        child = node.children[i];
+
+        if (getPositionType(child) === CSS_POSITION_ABSOLUTE &&
+            isPosDefined(child, leading[crossAxis])) {
+          // In case the child is absolutely positionned and has a
+          // top/left/bottom/right being set, we override all the previously
+          // computed positions to set it correctly.
+          child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) +
+            getLeadingBorder(node, crossAxis) +
+            getLeadingMargin(child, crossAxis);
+
+        } else {
+          var/*float*/ leadingCrossDim = leadingPaddingAndBorderCross;
+
+          // For a relative children, we're either using alignItems (parent) or
+          // alignSelf (child) in order to determine the position in the cross axis
+          if (getPositionType(child) === CSS_POSITION_RELATIVE) {
+            // This variable is intentionally re-defined as the code is transpiled to a block scope language
+            var/*css_align_t*/ alignItem = getAlignItem(node, child);
+            if (alignItem === CSS_ALIGN_STRETCH) {
+              // You can only stretch if the dimension has not already been set
+              // previously.
+              if (isUndefined(child.layout[dim[crossAxis]])) {
+                child.layout[dim[crossAxis]] = fmaxf(
+                  boundAxis(child, crossAxis, containerCrossAxis -
+                    paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)),
+                  // You never want to go smaller than padding
+                  getPaddingAndBorderAxis(child, crossAxis)
+                );
+              }
+            } else if (alignItem !== CSS_ALIGN_FLEX_START) {
+              // The remaining space between the parent dimensions+padding and child
+              // dimensions+margin.
+              var/*float*/ remainingCrossDim = containerCrossAxis -
+                paddingAndBorderAxisCross - getDimWithMargin(child, crossAxis);
+
+              if (alignItem === CSS_ALIGN_CENTER) {
+                leadingCrossDim += remainingCrossDim / 2;
+              } else { // CSS_ALIGN_FLEX_END
+                leadingCrossDim += remainingCrossDim;
+              }
+            }
+          }
+
+          // And we apply the position
+          child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim;
+
+          // Define the trailing position accordingly.
+          if (isCrossDimDefined) {
+            setTrailingPosition(node, child, crossAxis);
+          }
+        }
+      }
+
+      linesCrossDim += crossDim;
+      linesMainDim = fmaxf(linesMainDim, mainDim);
+      linesCount += 1;
+      startLine = endLine;
+    }
+
+    // <Loop E>
+    //
+    // Note(prenaux): More than one line, we need to layout the crossAxis
+    // according to alignContent.
+    //
+    // Note that we could probably remove <Loop D> and handle the one line case
+    // here too, but for the moment this is safer since it won't interfere with
+    // previously working code.
+    //
+    // See specs:
+    // http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm
+    // section 9.4
+    //
+    if (linesCount > 1 && isCrossDimDefined) {
+      var/*float*/ nodeCrossAxisInnerSize = node.layout[dim[crossAxis]] -
+          paddingAndBorderAxisCross;
+      var/*float*/ remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim;
+
+      var/*float*/ crossDimLead = 0;
+      var/*float*/ currentLead = leadingPaddingAndBorderCross;
+
+      var/*css_align_t*/ alignContent = getAlignContent(node);
+      if (alignContent === CSS_ALIGN_FLEX_END) {
+        currentLead += remainingAlignContentDim;
+      } else if (alignContent === CSS_ALIGN_CENTER) {
+        currentLead += remainingAlignContentDim / 2;
+      } else if (alignContent === CSS_ALIGN_STRETCH) {
+        if (nodeCrossAxisInnerSize > linesCrossDim) {
+          crossDimLead = (remainingAlignContentDim / linesCount);
+        }
+      }
+
+      var/*int*/ endIndex = 0;
+      for (i = 0; i < linesCount; ++i) {
+        var/*int*/ startIndex = endIndex;
+
+        // compute the line's height and find the endIndex
+        var/*float*/ lineHeight = 0;
+        for (ii = startIndex; ii < childCount; ++ii) {
+          child = node.children[ii];
+          if (getPositionType(child) !== CSS_POSITION_RELATIVE) {
+            continue;
+          }
+          if (child.lineIndex !== i) {
+            break;
+          }
+          if (!isUndefined(child.layout[dim[crossAxis]])) {
+            lineHeight = fmaxf(
+              lineHeight,
+              child.layout[dim[crossAxis]] + getMarginAxis(child, crossAxis)
+            );
+          }
+        }
+        endIndex = ii;
+        lineHeight += crossDimLead;
+
+        for (ii = startIndex; ii < endIndex; ++ii) {
+          child = node.children[ii];
+          if (getPositionType(child) !== CSS_POSITION_RELATIVE) {
+            continue;
+          }
+
+          var/*css_align_t*/ alignContentAlignItem = getAlignItem(node, child);
+          if (alignContentAlignItem === CSS_ALIGN_FLEX_START) {
+            child.layout[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis);
+          } else if (alignContentAlignItem === CSS_ALIGN_FLEX_END) {
+            child.layout[pos[crossAxis]] = currentLead + lineHeight - getTrailingMargin(child, crossAxis) - child.layout[dim[crossAxis]];
+          } else if (alignContentAlignItem === CSS_ALIGN_CENTER) {
+            var/*float*/ childHeight = child.layout[dim[crossAxis]];
+            child.layout[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2;
+          } else if (alignContentAlignItem === CSS_ALIGN_STRETCH) {
+            child.layout[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis);
+            // TODO(prenaux): Correctly set the height of items with undefined
+            //                (auto) crossAxis dimension.
+          }
+        }
+
+        currentLead += lineHeight;
+      }
+    }
+
+    var/*bool*/ needsMainTrailingPos = false;
+    var/*bool*/ needsCrossTrailingPos = false;
+
+    // If the user didn't specify a width or height, and it has not been set
+    // by the container, then we set it via the children.
+    if (!isMainDimDefined) {
+      node.layout[dim[mainAxis]] = fmaxf(
+        // We're missing the last padding at this point to get the final
+        // dimension
+        boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)),
+        // We can never assign a width smaller than the padding and borders
+        paddingAndBorderAxisMain
+      );
+
+      if (mainAxis === CSS_FLEX_DIRECTION_ROW_REVERSE ||
+          mainAxis === CSS_FLEX_DIRECTION_COLUMN_REVERSE) {
+        needsMainTrailingPos = true;
+      }
+    }
+
+    if (!isCrossDimDefined) {
+      node.layout[dim[crossAxis]] = fmaxf(
+        // For the cross dim, we add both sides at the end because the value
+        // is aggregate via a max function. Intermediate negative values
+        // can mess this computation otherwise
+        boundAxis(node, crossAxis, linesCrossDim + paddingAndBorderAxisCross),
+        paddingAndBorderAxisCross
+      );
+
+      if (crossAxis === CSS_FLEX_DIRECTION_ROW_REVERSE ||
+          crossAxis === CSS_FLEX_DIRECTION_COLUMN_REVERSE) {
+        needsCrossTrailingPos = true;
+      }
+    }
+
+    // <Loop F> Set trailing position if necessary
+    if (needsMainTrailingPos || needsCrossTrailingPos) {
+      for (i = 0; i < childCount; ++i) {
+        child = node.children[i];
+
+        if (needsMainTrailingPos) {
+          setTrailingPosition(node, child, mainAxis);
+        }
+
+        if (needsCrossTrailingPos) {
+          setTrailingPosition(node, child, crossAxis);
+        }
+      }
+    }
+
+    // <Loop G> Calculate dimensions for absolutely positioned elements
+    currentAbsoluteChild = firstAbsoluteChild;
+    while (currentAbsoluteChild !== null) {
+      // Pre-fill dimensions when using absolute position and both offsets for
+      // the axis are defined (either both left and right or top and bottom).
+      for (ii = 0; ii < 2; ii++) {
+        axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
+
+        if (!isUndefined(node.layout[dim[axis]]) &&
+            !isDimDefined(currentAbsoluteChild, axis) &&
+            isPosDefined(currentAbsoluteChild, leading[axis]) &&
+            isPosDefined(currentAbsoluteChild, trailing[axis])) {
+          currentAbsoluteChild.layout[dim[axis]] = fmaxf(
+            boundAxis(currentAbsoluteChild, axis, node.layout[dim[axis]] -
+              getBorderAxis(node, axis) -
+              getMarginAxis(currentAbsoluteChild, axis) -
+              getPosition(currentAbsoluteChild, leading[axis]) -
+              getPosition(currentAbsoluteChild, trailing[axis])
+            ),
+            // You never want to go smaller than padding
+            getPaddingAndBorderAxis(currentAbsoluteChild, axis)
+          );
+        }
+
+        if (isPosDefined(currentAbsoluteChild, trailing[axis]) &&
+            !isPosDefined(currentAbsoluteChild, leading[axis])) {
+          currentAbsoluteChild.layout[leading[axis]] =
+            node.layout[dim[axis]] -
+            currentAbsoluteChild.layout[dim[axis]] -
+            getPosition(currentAbsoluteChild, trailing[axis]);
+        }
+      }
+
+      child = currentAbsoluteChild;
+      currentAbsoluteChild = currentAbsoluteChild.nextAbsoluteChild;
+      child.nextAbsoluteChild = null;
+    }
+  }
+
+  function layoutNode(node, parentMaxWidth, parentDirection) {
+    node.shouldUpdate = true;
+
+    var direction = node.style.direction || CSS_DIRECTION_LTR;
+    var skipLayout =
+      !node.isDirty &&
+      node.lastLayout &&
+      node.lastLayout.requestedHeight === node.layout.height &&
+      node.lastLayout.requestedWidth === node.layout.width &&
+      node.lastLayout.parentMaxWidth === parentMaxWidth &&
+      node.lastLayout.direction === direction;
+
+    if (skipLayout) {
+      node.layout.width = node.lastLayout.width;
+      node.layout.height = node.lastLayout.height;
+      node.layout.top = node.lastLayout.top;
+      node.layout.left = node.lastLayout.left;
+    } else {
+      if (!node.lastLayout) {
+        node.lastLayout = {};
+      }
+
+      node.lastLayout.requestedWidth = node.layout.width;
+      node.lastLayout.requestedHeight = node.layout.height;
+      node.lastLayout.parentMaxWidth = parentMaxWidth;
+      node.lastLayout.direction = direction;
+
+      // Reset child layouts
+      node.children.forEach(function(child) {
+        child.layout.width = undefined;
+        child.layout.height = undefined;
+        child.layout.top = 0;
+        child.layout.left = 0;
+      });
+
+      layoutNodeImpl(node, parentMaxWidth, parentDirection);
+
+      node.lastLayout.width = node.layout.width;
+      node.lastLayout.height = node.layout.height;
+      node.lastLayout.top = node.layout.top;
+      node.lastLayout.left = node.layout.left;
+    }
+  }
+
+  return {
+    layoutNodeImpl: layoutNodeImpl,
+    computeLayout: layoutNode,
+    fillNodes: fillNodes
+  };
+})();
+
+export default function(node) {
+  // disabling ESLint because this code relies on the above include
+  computeLayout.fillNodes(node);
+  computeLayout.computeLayout(node);
+};

+ 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

+ 31 - 1
package-lock.json

@@ -1,3 +1,33 @@
 {
-  "lockfileVersion": 1
+  "name": "charge_miniapp",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "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=="
+    },
+    "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==",
+      "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==",
+      "dependencies": {
+        "widget-ui": "^1.0.2"
+      }
+    }
+  }
 }

+ 13 - 0
package.json

@@ -0,0 +1,13 @@
+{
+    "id": "r-canvas",
+    "name": "海报生成,随心所欲绘制样式,原生canvas方法的二次封装,自定义函数,持续更新",
+    "version": "1.3.1",
+    "description": "图片不失帧,保留原有画质,canvas方法扩展,暴露原生实例,可自行扩展,最好用的canvas插件",
+    "keywords": [
+        "canvas",
+        "画布生成图片",
+        "绘制图片",
+        "商品海报",
+        "朋友圈海报"
+    ]
+}

+ 55 - 65
pages.json

@@ -1,118 +1,108 @@
 {
 	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
 		{
-			"path" : "pages/index/index",
-			"style" : 
-			{
-				"navigationBarTitleText" : "首页"
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "首页"
 			}
 		},
 		{
-			"path" : "pages/map/map",
-			"style" : 
-			{
-				"navigationBarTitleText" : "地图模式"
+			"path": "pages/map/map",
+			"style": {
+				"navigationBarTitleText": "地图模式"
 			}
 		},
 		{
-			"path" : "pages/my/my",
-			"style" : 
-			{
-				"navigationBarTitleText" : "个人中心"
+			"path": "pages/my/my",
+			"style": {
+				"navigationBarTitleText": "个人中心"
 			}
 		},
 		{
-			"path" : "pages/site/site",
-			"style" : 
-			{
-				"navigationBarTitleText" : "站点详情"
+			"path": "pages/site/site",
+			"style": {
+				"navigationBarTitleText": "站点详情"
 			}
 		},
 		{
-			"path" : "pages/site-more/site-more",
-			"style" : 
-			{
-				"navigationBarTitleText" : "电站价格"
+			"path": "pages/site-more/site-more",
+			"style": {
+				"navigationBarTitleText": "电站价格"
 			}
 		},
 		{
-			"path" : "pages/charging/charging",
-			"style" : 
-			{
-				"navigationBarTitleText" : "正在充电"
+			"path": "pages/charging/charging",
+			"style": {
+				"navigationBarTitleText": "正在充电"
 			}
 		},
 		{
-			"path" : "pages/order-detail/order-detail",
-			"style" : 
-			{
-				"navigationBarTitleText" : "订单详情"
+			"path": "pages/order-detail/order-detail",
+			"style": {
+				"navigationBarTitleText": "订单详情"
 			}
 		},
 		{
-			"path" : "pages/terminal/terminal",
-			"style" : 
-			{
-				"navigationBarTitleText" : "充电终端"
+			"path": "pages/terminal/terminal",
+			"style": {
+				"navigationBarTitleText": "充电终端"
 			}
 		},
 		{
-			"path" : "pages/coupon-buy/coupon-buy",
-			"style" : 
-			{
-				"navigationBarTitleText" : "购券中心"
+			"path": "pages/coupon-buy/coupon-buy",
+			"style": {
+				"navigationBarTitleText": "购券中心"
 			}
 		},
 		{
-			"path" : "pages/login/login",
-			"style" : 
-			{
-				"navigationBarTitleText" : "用户登录"
+			"path": "pages/login/login",
+			"style": {
+				"navigationBarTitleText": "用户登录"
 			}
 		},
 		{
-			"path" : "pages/recharge-log/recharge-log",
-			"style" : 
-			{
-				"navigationBarTitleText" : "购券记录"
+			"path": "pages/recharge-log/recharge-log",
+			"style": {
+				"navigationBarTitleText": "购券记录"
 			}
 		},
 		{
-			"path" : "pages/feedback/feedback",
-			"style" : 
-			{
-				"navigationBarTitleText" : ""
+			"path": "pages/feedback/feedback",
+			"style": {
+				"navigationBarTitleText": ""
 			}
 		},
 		{
-			"path" : "pages/order/order",
-			"style" : 
-			{
-				"navigationBarTitleText" : "充电订单"
+			"path": "pages/order/order",
+			"style": {
+				"navigationBarTitleText": "充电订单"
 			}
 		},
 		{
-			"path" : "pages/search/search",
-			"style" : 
-			{
-				"navigationBarTitleText" : "站点搜索"
+			"path": "pages/search/search",
+			"style": {
+				"navigationBarTitleText": "站点搜索"
 			}
 		},
 		{
-			"path" : "pages/web/web",
-			"style" : 
-			{
-				"navigationBarTitleText" : "",
+			"path": "pages/Invite-staff/Invite-staff",
+			"style": {
+				"navigationBarTitleText": "邀请员工"
+			}
+		},
+		{
+			"path": "pages/web/web",
+			"style": {
+				"navigationBarTitleText": "",
 				"navigationStyle": "default",
 				"navigationBarBackgroundColor": "#fff",
 				"backgroundColor": "#fff"
 			}
 		},
 		{
-			"path" : "pages/feedback-reply/feedback-reply",
-			"style" : 
-			{
-				"navigationBarTitleText" : "反馈答复"
+			"path": "pages/feedback-reply/feedback-reply",
+			"style": {
+				"navigationBarTitleText": "反馈答复"
 			}
 		}
 	],
@@ -124,4 +114,4 @@
 		"navigationStyle": "custom"
 	},
 	"uniIdRouter": {}
-}
+}

+ 71 - 0
pages/Invite-staff/Invite-staff.css

@@ -0,0 +1,71 @@
+ax-body {
+	display: block;
+	height: 100%;
+}
+
+.invite-staff-bg {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	z-index:-1000;
+}
+
+.invite-staff-bg image {
+	display: block;
+	width: 100%;
+}
+.invite-staff-box{
+	margin-top: 230rpx;
+	text-align: center;
+}
+.invite-staff-box .attend-card-box{
+	position: relative;
+	left: 24rpx;
+	width: 700rpx;
+	height: 800rpx;
+	/* background-color: #aaaa7f; */
+}
+.invite-staff-box .attend-card-box .userinfo-box{
+	position: absolute;
+	top: -50rpx;
+	width: 100%;
+}
+.invite-staff-box .attend-card-box .userinfo-box .user-avatar{
+	width: 120rpx;
+	height: 120rpx;
+	border-radius: 50%;
+}
+.invite-staff-box .attend-card-box .card-img-box image {
+	width: 680rpx;
+}
+.invite-staff-box .attend-card-box .card-qrcode-box{
+	position: absolute;
+	top: 328rpx;
+	width: 100%;
+}
+.invite-staff-box .attend-card-box .card-qrcode-box .card-qrcode{
+	width: 200rpx;
+	height: 200rpx;
+}
+.invite-staff-box .attend-card-box .tips-text{
+	position: absolute;
+	width: 100%;
+	top: 678rpx;
+	font-size: 28rpx;
+	color: #181818;
+}
+.invite-staff-box .bottom-tips-text{
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.invite-staff-box .bottom-tips-text image{
+	width: 48rpx;
+	height: 48rpx;
+}
+.invite-staff-box .bottom-tips-text text{
+	font-size: 28rpx;
+	color: #2B303A;
+	margin-left: 12rpx;
+}

+ 142 - 0
pages/Invite-staff/Invite-staff.vue

@@ -0,0 +1,142 @@
+<template>
+	<ax-body blank="0" title="">
+		<view class="invite-staff-bg">
+			<image :src="bg_img" mode="widthFix"></image>
+		</view>
+		<view class="invite-staff-box">
+			<view class="attend-card-box" @longpress="longpressTap">
+				<view class="userinfo-box">
+					<image class="user-avatar" :src="avatar_img" mode=""></image>
+					<view class="user-nikename">{{userinfo.nickName?userinfo.nickName:'匿名'}}</view>
+				</view>
+				<view class="card-img-box">
+					<image :src="card_img" mode="widthFix"></image>
+				</view>
+				<view class="card-qrcode-box">
+					<!-- <image class="card-qrcode" src="@/static/img/1111.png"></image> -->
+				</view>
+				<view class="tips-text">长按识别二维码</view>
+			</view>
+			<view class="bottom-tips-text">
+				<image src="@/static/img/tips-icon.svg" mode=""></image>
+				<text>长按上方海报发送给好友</text>
+			</view>
+		</view>
+		<r-canvas ref="rCanvas"></r-canvas>
+	</ax-body>
+</template>
+<script>
+	import rCanvas from "@/components/r-canvas/r-canvas.vue"
+	export default {
+		components: {
+			rCanvas
+		},
+		data() {
+			return {
+				bg_img: 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/yzBqSYi5PEsId74406272837cf072c83825d28c270d3.png/1.png',
+				card_img: 'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/nCHfvld10tLIbce45895fa953e8be335bfb17c9ab29b.png/1.png',
+				avatar_img:'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/IBN033tZBgCeb082e48d7fc55ab5c8e84b890c41f169.png/1.png',
+				isImageLoaded: false,
+				userinfo:{}
+			}
+		},
+		onLoad() {
+			this.userinfo=this.$app.storage.get('USER_INFO')
+		},
+		onReady() {},
+		methods: {
+			longpressTap() {
+				this.get_downloadImg()
+			},
+			async get_downloadImg() {
+				this.$nextTick(async () => {
+					await this.$refs.rCanvas.init({
+						canvas_id: "rCanvas"
+					})
+					await this.$refs.rCanvas.setCanvasWidth(319)
+					await this.$refs.rCanvas.setCanvasHeight(414)
+					// 背景卡片
+					await this.$refs.rCanvas.drawImage({
+						url: this.card_img,
+						x: -10,
+						y: 30,
+						w: 338,
+						h: 395
+					}).catch(err_msg => {
+						uni.showToast({
+							title: err_msg,
+							icon: "none"
+						})
+					})
+					// 头像
+					await this.$refs.rCanvas.drawImage({
+						url:this.avatar_img,
+						x:134,
+						y:0,
+						w: 50,
+						h: 50
+					}).catch(err_msg => {
+						uni.showToast({
+							title: err_msg,
+							icon: "none"
+						})
+					})
+					// 二维码
+					await this.$refs.rCanvas.drawImage({
+						url:'/static/img/1111.png',
+						x:134,
+						y:0,
+						w: 50,
+						h: 50
+					}).catch(err_msg => {
+						uni.showToast({
+							title: err_msg,
+							icon: "none"
+						})
+					})
+					//文字
+					await this.$refs.rCanvas.drawText({
+						text:this.userinfo.nickName?userinfo.nickName:'匿名用户',
+						max_width: 0,
+						x:132,
+						y:70,
+						font_color: "#181818",
+						font_size: 14
+					}).catch(err_msg => {
+						uni.showToast({
+							title: err_msg,
+							icon: "none"
+						})
+					})
+					await this.$refs.rCanvas.drawText({
+						text: "长按识别二维码",
+						max_width: 0,
+						x:108,
+						y:386,
+						font_color: "#181818",
+						font_size: 14
+					}).catch(err_msg => {
+						uni.showToast({
+							title: err_msg,
+							icon: "none"
+						})
+					})
+					// 生成海报
+					await this.$refs.rCanvas.draw((res) => {
+						//res.tempFilePath:生成成功,返回base64图片
+						uni.showShareImageMenu({
+							path: res.tempFilePath,
+							success: (res) => console.log('分享成功', res),
+							fail: (err) => console.error('分享失败', err),
+						});
+						// this.$refs.rCanvas.saveImage(res.tempFilePath)
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@import url("Invite-staff.css");
+</style>

+ 60 - 41
pages/coupon-buy/coupon-buy.vue

@@ -1,6 +1,8 @@
 <template>
 	<ax-body>
-		<view class="page-background"><image src="@/static/img/page-bg01.png" mode="widthFix"></image></view>
+		<view class="page-background">
+			<image src="@/static/img/page-bg01.png" mode="widthFix"></image>
+		</view>
 		<view class="body">
 			<!-- 统计卡 -->
 			<view class="app-flex c-between">
@@ -17,16 +19,25 @@
 			</view>
 			<!-- 购券 -->
 			<view class="card">
-				<view class="title"><view>请选择抵扣券</view></view>
+				<view class="title">
+					<view>请选择抵扣券</view>
+				</view>
 				<view class="coupons">
-					<view v-for="(item,index) in coupons.data" :key="index" :class="{active:coupons.index==index}" @click="choose(item,index)" class="coupon-item">
+					<view v-for="(item,index) in coupons.data" :key="index" :class="{active:coupons.index==index}"
+						@click="choose(item,index)" class="coupon-item">
 						<view class="ticket">
-							<view class="value"><text class="val">{{item.levelMoney}}</text><view class="unit">元</view></view>
-							<view class="line-wrap"><view class="circle"></view><view class="line"></view><view class="circle"></view></view>
+							<view class="value"><text class="val">{{item.levelMoney}}</text>
+								<view class="unit">元</view>
+							</view>
+							<view class="line-wrap">
+								<view class="circle"></view>
+								<view class="line"></view>
+								<view class="circle"></view>
+							</view>
 							<view class="margin"></view>
 						</view>
 						<view class="trapezium"></view>
-						<view class="text">充电优惠券</view>
+						<view class="text">充电券</view>
 					</view>
 				</view>
 			</view>
@@ -36,6 +47,8 @@
 				<view class="li">1. 抵扣券仅用于充电结算中抵扣资费,未使用完的余量,可手动发起退还</view>
 				<view class="li">2. 抵扣券为专属专用不可转赠和出售</view>
 				<view class="li">3. 抵扣券金额未抵扣完结,可累计到下次继续抵扣</view>
+				<view class="li">4. 使用时可抵扣按照当前电价进行预估计算</view>
+				<view class="li">5. 实际结算价以具体充电时段为准</view>
 			</view>
 			<view class="footer">
 				<button @click="pay()" class="pay" :disabled="coupons.index<0">立即支付购买</button>
@@ -49,8 +62,8 @@
 	export default {
 		data() {
 			return {
-				userinfo:{},
-				coupons:{
+				userinfo: {},
+				coupons: {
 					index: -1,
 					data: []
 				}
@@ -61,54 +74,60 @@
 			this.getLevel()
 		},
 		methods: {
-			getLevel(){
-				this.$api.base("post","/orderApi/getReChargeLevel",{},{}).then(res=>{
+			getLevel() {
+				this.$api.base("post", "/orderApi/getReChargeLevel", {}, {}).then(res => {
 					this.coupons.data = res.levels;
 				})
 			},
-			choose(item,index){
-				this.coupons.index = this.coupons.index!=index ? index : -1;
+			choose(item, index) {
+				this.coupons.index = this.coupons.index != index ? index : -1;
 			},
-			pay(){
-				
-				if(!this.userinfo.phone){
+			pay() {
+
+				if (!this.userinfo.phone) {
 					this.$app.url.goto('/pages/login/login')
 					return;
 				}
-				
-				
-				this.$api.base("post","/orderApi/addOrder",{levelId:this.coupons.data[this.coupons.index].id},{}).then(addRes=>{
-					if(addRes.orderId){
-						this.$api.base("post","/orderApi/payOrder",{orderId:addRes.orderId},{}).then(res=>{
+
+
+				this.$api.base("post", "/orderApi/addOrder", {
+					levelId: this.coupons.data[this.coupons.index].id
+				}, {}).then(addRes => {
+					if (addRes.orderId) {
+						this.$api.base("post", "/orderApi/payOrder", {
+							orderId: addRes.orderId
+						}, {}).then(res => {
 							var payInfo = JSON.parse(res.wx.wx.pay_info)
 							uni.requestPayment({
-							    provider: 'wxpay',
-							    timeStamp: payInfo.timeStamp,
-							    nonceStr: payInfo.nonceStr,
-							    package: payInfo.package,
-							    signType:  payInfo.signType,
-							    paySign: payInfo.paySign,
-							    success: (res)=> {
-									console.log('success:',res);
+								provider: 'wxpay',
+								timeStamp: payInfo.timeStamp,
+								nonceStr: payInfo.nonceStr,
+								package: payInfo.package,
+								signType: payInfo.signType,
+								paySign: payInfo.paySign,
+								success: (res) => {
+									console.log('success:', res);
 									//注册一个用户支付成功后点确定的事件
-									this.$app.popup.alert('支付成功','温馨提示',{showCancel:false}).then(()=>{
+									this.$app.popup.alert('支付成功', '温馨提示', {
+										showCancel: false
+									}).then(() => {
 										this.$app.url.back()
 									});
-							    },
-							    fail: (err)=> {
-									 console.log('fail:',err);
-									 //注册一个用户取消支付的事件
-							       
-							    }
+								},
+								fail: (err) => {
+									console.log('fail:', err);
+									//注册一个用户取消支付的事件
+
+								}
 							});
-							
+
 						})
 					}
 				})
-				
+
 			},
-			getMyAccount(){
-				this.$api.base("post","/userApi/getUserAccount",{},{}).then(res=>{
+			getMyAccount() {
+				this.$api.base("post", "/userApi/getUserAccount", {}, {}).then(res => {
 					this.userinfo = res.accountInfo
 				})
 			},
@@ -117,5 +136,5 @@
 </script>
 
 <style scoped>
-@import url("coupon-buy.css");
-</style>
+	@import url("coupon-buy.css");
+</style>

+ 149 - 67
pages/index/index.css

@@ -1,14 +1,14 @@
-page{
+page {
 	font-size: 16px;
 }
 
-ax-body{
+ax-body {
 	display: block;
 	height: 100%;
-	background: linear-gradient(to bottom,#C7FFFD,#FFF,#E6F3FF,#8BECFF,#72B8FE,#86D3FD);
+	background: linear-gradient(to bottom, #C7FFFD, #FFF, #E6F3FF, #8BECFF, #72B8FE, #86D3FD);
 }
 
-ax-body >>> .ax-body .__root{
+ax-body>>>.ax-body .__root {
 	display: flex;
 	flex-direction: column;
 	padding-left: 0 !important;
@@ -17,27 +17,31 @@ ax-body >>> .ax-body .__root{
 	overflow: hidden !important;
 	padding-bottom: var(--app-navigation-heiht) !important;
 }
-.main-scroll-wrap{
+
+.main-scroll-wrap {
 	flex: 1;
 	position: relative;
 }
-.root-scroll{
+
+.root-scroll {
 	position: absolute;
 	top: 0;
 	left: 0;
 	right: 0;
 	bottom: 0;
 }
-.contet-root{
+
+.contet-root {
 	height: 100%;
 }
-.base{
+
+.base {
 	padding-left: 10px;
 	padding-right: 10px;
 }
 
 /* 导航栏 */
-app-navigation{
+app-navigation {
 	display: block;
 	position: fixed;
 	left: 0;
@@ -46,31 +50,35 @@ app-navigation{
 }
 
 /* 标题栏 */
-.titlebar{
+.titlebar {
 	display: flex;
 	align-items: center;
 	padding-left: 3px;
 }
-.titlebar .page-title{
+
+.titlebar .page-title {
 	width: 133px;
 	height: 33px;
 }
-.titlebar .page-subtitle{
+
+.titlebar .page-subtitle {
 	font-size: 12px;
 }
 
 /* 定位城市 */
-.search-view{
+.search-view {
 	position: sticky;
 	top: 0;
 	z-index: 90;
 }
-.locate-city{
+
+.locate-city {
 	display: flex;
 	align-items: center;
 	margin-right: 10px;
 }
-.locate-city ._icon{
+
+.locate-city ._icon {
 	display: inline-block;
 	width: 20px;
 	height: 20px;
@@ -78,7 +86,7 @@ app-navigation{
 }
 
 /* 搜索条 */
-.search-bar{
+.search-bar {
 	flex: 1;
 	display: flex;
 	align-items: center;
@@ -88,13 +96,15 @@ app-navigation{
 	border: 1px solid #2B303A;
 	background-color: #fff;
 }
-.search-bar input{
+
+.search-bar input {
 	flex: 1;
 	display: block;
 	font-size: 14px;
 	background-color: transparent;
 }
-.search-bar ._icon-search{
+
+.search-bar ._icon-search {
 	display: inline-block;
 	width: 20px;
 	height: 20px;
@@ -102,155 +112,181 @@ app-navigation{
 }
 
 /* 快捷栏 */
-.shortcut-bar{
+.shortcut-bar {
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
 	padding: 15px 10px;
 }
-.shortcut-bar ._item{
+
+.shortcut-bar ._item {
 	display: flex;
 	align-items: center;
 	justify-content: center;
 	flex-direction: column;
 }
-.shortcut-bar ._item ._icon{
+
+.shortcut-bar ._item ._icon {
 	width: 40px;
 	height: 40px;
 }
-.shortcut-bar ._item ._name{
+
+.shortcut-bar ._item ._name {
 	font-size: 12px;
 	margin-top: 6px;
 }
 
 /* 版头广告 */
-.banner{
+.banner {
 	width: 100%;
 	height: calc((100vw - 20px) * (5/27));
 }
-.banner .swiper-item{
+
+.banner .swiper-item {
 	display: flex;
 	width: 100%;
 }
-.banner .swiper-item .swiper-item-image{
+
+.banner .swiper-item .swiper-item-image {
 	display: block;
 	width: 100%;
 	border-radius: 5px;
 }
 
 /* 选项条 */
-.options-bar{
+.options-bar {
 	align-items: center;
 	padding: 15px 0px;
 	font-size: 14px;
 }
-.options-bar .option-item{
+
+.options-bar .option-item {
 	display: flex;
 	align-items: center;
 	justify-content: center;
 	flex-direction: column;
 	font-size: inherit;
 }
-.options-bar .option-item + .option-item{
+
+.options-bar .option-item+.option-item {
 	margin-left: 30px;
 }
-.options-bar .option-item.active{
+
+.options-bar .option-item.active {
 	font-size: 18px;
 	font-weight: bold;
 }
-.options-bar .option-item::after{
+
+.options-bar .option-item::after {
 	content: '';
 	display: block;
 	width: 30px;
 	height: 6px;
 	transform: translateY(5px) scaleX(0);
 	border-radius: 10pc;
-	background: linear-gradient(to right,#8FF8FB,#47AEFF);
+	background: linear-gradient(to right, #8FF8FB, #47AEFF);
 	transition: all 400ms ease;
 }
-.options-bar .option-item.active::after{
+
+.options-bar .option-item.active::after {
 	transform: translateY(5px) scaleX(1);
 }
-.options-bar .separ{
+
+.options-bar .separ {
 	flex: 1;
 }
-.options-bar .separ::before{
+
+.options-bar .separ::before {
 	content: '';
 	display: block;
 	height: 13px;
 	border-left: 1px solid #3EB6F8;
 }
-.options-bar .map-mode{
+
+.options-bar .map-mode {
 	color: #3EB6F8;
 }
-.options-bar .map-mode ._icon{
+
+.options-bar .map-mode ._icon {
 	width: 17px;
 	height: 17px;
 	margin-right: 5px;
 }
 
 /* 电站列表 */
-.list-scroll-wrap{
+.list-scroll-wrap {
 	height: var(--list-heiht);
 	position: relative;
 }
-.list-scroll-wrap > .list-scroll{
+
+.list-scroll-wrap>.list-scroll {
 	position: absolute;
 	top: 0;
 	left: 0;
 	right: 0;
 	bottom: 0;
 }
-.list{
+
+.list {
 	padding: 10px;
 }
-.list .item{
+
+.list .item {
 	display: block;
 	border-radius: 8px;
 	background-color: #fff;
 	box-shadow: 0 1px 10px rgba(0, 39, 52, 0.1);
 	overflow: hidden;
 }
-.list .item + .item{
+
+.list .item+.item {
 	margin-top: 10px;
 }
-.list .item .contet{
+
+.list .item .contet {
 	padding: 10px;
 	background: #F6F9FE url('@/static/img/charging_station_item_background.png') no-repeat center;
 }
-.list .item .name{
+
+.list .item .name {
 	display: flex;
 	font-weight: bold;
 }
-.list .item .name > .txt{
+
+.list .item .name>.txt {
 	flex: 1;
 	line-height: 1.5em;
 }
-.list .item .name > .icon{
+
+.list .item .name>.icon {
 	display: flex;
 	align-items: center;
 	justify-content: center;
 	width: 20px;
 	height: 20px;
 	border-radius: 4px;
-	background: linear-gradient(to bottom,#2A67F0,#769FFC);
+	background: linear-gradient(to bottom, #2A67F0, #769FFC);
 	margin-right: 10px;
 }
-.list .item .name > .icon image{
+
+.list .item .name>.icon image {
 	display: block;
 	width: 10px;
 }
-.list .item .parkade{
+
+.list .item .parkade {
 	display: flex;
 	font-size: 12px;
 	color: #aaa;
 	margin-top: 15px;
 }
-.list .item .parkade > .txt{
+
+.list .item .parkade>.txt {
 	flex: 1;
 	line-height: 1.5em;
 }
-.list .item .parkade > .icon{
+
+.list .item .parkade>.icon {
 	display: inline-flex;
 	align-items: center;
 	justify-content: center;
@@ -259,19 +295,23 @@ app-navigation{
 	background-color: #5BE7FF;
 	margin-right: 10px;
 }
-.list .item .parkade > .icon image{
+
+.list .item .parkade>.icon image {
 	display: block;
 	width: 10px;
 }
-.list .item .info{
+
+.list .item .info {
 	margin-top: 15px;
 }
-.list .item .info .charge{
+
+.list .item .info .charge {
 	display: inline-flex;
 	align-items: center;
 	justify-content: center;
 }
-.list .item .info .charge > .icon{
+
+.list .item .info .charge>.icon {
 	display: inline-flex;
 	align-items: center;
 	justify-content: center;
@@ -280,25 +320,31 @@ app-navigation{
 	color: #fff;
 	font-size: 13px;
 	border-radius: 4px;
-	background: linear-gradient(to bottom,#4FEF86,#00AA3A);
+	background: linear-gradient(to bottom, #4FEF86, #00AA3A);
 }
-.list .item .info .charge > .icon.blue{
-	background: linear-gradient(to bottom,#8EB1FF,#3071FF);
+
+.list .item .info .charge>.icon.blue {
+	background: linear-gradient(to bottom, #8EB1FF, #3071FF);
 }
-.list .item .info .charge > .value{
+
+.list .item .info .charge>.value {
 	margin-left: 5px;
 }
-.list .item .info .charge > .max{
+
+.list .item .info .charge>.max {
 	color: #aaa;
 	font-size: 12px;
 }
-.list .item .info .charge > .max::before{
+
+.list .item .info .charge>.max::before {
 	content: '/';
 }
-.list .item .info .charge + .charge{
+
+.list .item .info .charge+.charge {
 	margin-left: 30px;
 }
-.list .item .info .distance{
+
+.list .item .info .distance {
 	display: flex;
 	align-items: center;
 	height: 22px;
@@ -309,7 +355,8 @@ app-navigation{
 	padding-right: 7px;
 	overflow: hidden;
 }
-.list .item .info .distance > .icon{
+
+.list .item .info .distance>.icon {
 	display: inline-flex;
 	align-items: center;
 	justify-content: center;
@@ -318,11 +365,13 @@ app-navigation{
 	background-color: #3EB6F8;
 	margin-right: 5px;
 }
-.list .item .info .distance > .icon image{
+
+.list .item .info .distance>.icon image {
 	display: block;
 	width: 10px;
 }
-.list .item .price{
+
+.list .item .price {
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
@@ -332,10 +381,43 @@ app-navigation{
 	padding: 0 10px;
 	box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
 }
-.list .item .price .value{
+
+.list .item .price .value {
 	font-size: 24px;
 	font-weight: bold;
 }
-.list .item .price .unit{
+
+.list .item .price .unit {
 	margin-left: 10px;
+}
+
+.card-bottom-text {
+	font-weight: bold;
+	font-size: 20px;
+}
+
+.card-bottom-text .mini-text {
+	margin-left: 6rpx;
+	font-size: 12px;
+}
+
+.operation-price-btn {
+	margin-left: 20rpx;
+	width: 140rpx;
+	height: 40rpx;
+	background: #FF6457;
+	border-radius: 22rpx 22rpx 22rpx 22rpx;
+	font-style: italic; 
+	text-align: center;
+	line-height: 40rpx;
+	font-weight: 400;
+	font-size: 22rpx;
+	color: #FFFFFF;
+}
+
+.ordinary-price {
+	margin-left: 12rpx;
+	font-size: 24rpx;
+	color: #AAAAAA;
+	text-decoration: line-through;
 }

+ 12 - 1
pages/index/index.vue

@@ -100,10 +100,20 @@
 										</view>
 									</view>
 									<view class="price">
-										<view class="app-flex middle" style="color: #FF5D50;">
+										<view class="app-flex middle" style="color: #FF5D50;" v-if="!isOperation">
 											<text class="value">{{item.params.nowPrice?item.params.nowPrice.toFixed(4):"0.0000"}}</text>
 											<text class="unit">元/度</text>
 										</view>
+										<view class="app-flex middle" v-else>
+											<view class="card-bottom-text">
+												<text>1.0026</text>
+												<text class="mini-text">元/度</text>
+											</view>
+											<view class="operation-price-btn">
+												企业专享价
+											</view>
+											<view class="ordinary-price">{{item.params.nowPrice?item.params.nowPrice.toFixed(4):"0.0000"}}</view>
+										</view>
 										<view>{{item.params.priceShow}}</view>
 									</view>
 								</view>
@@ -160,6 +170,7 @@
 		},
 		data() {
 			return {
+				isOperation:false,
 				// 导航栏高度
 				appNavigationHeight: 0,
 				// 页面滚动锁

+ 23 - 15
pages/login/login.vue

@@ -54,27 +54,35 @@
 				uni.openPrivacyContract();
 			},
 			onGetPhoneNumber(e) {
-				let channelUrl='http://192.168.2.21:9120/zs/channel/admin/'
+				console.log(e);
+				let channelUrl='http://192.168.110.241:9120/zs/channel/admin/'
+				// let channelUrl='https://channel-api.zonelife.cn/zs/channel/admin/'
 				if (e.detail.code) {
 					this.$api.base("post", '/userApi/getPhone', {
 						code: e.detail.code
 					}).then(async res => {
 						await this.$app.storage.set('USER_INFO', res.userInfo);
 						// 识别用户是否通过分销码进入小程序
-						uni.request({
-							url:channelUrl+'ums/umsAdminUser/distributionBindUser',
-							method: 'POST',
-							header: {
-								'content-type': 'application/json'
-							},
-							data: {
-								userId: res.userInfo.id,
-								adminUserId: uni.getStorageSync('adminUserId'),
-							},
-							success: (res) => {
-								console.log('扫码接口调用');
-							}
-						});
+						if(uni.getStorageSync('ADMIN_USERID')){
+							uni.request({
+								url:channelUrl+'ums/umsAdminUser/distributionBindUser',
+								method: 'POST',
+								header: {
+									'content-type': 'application/json'
+								},
+								data: {
+									userId: res.userInfo.id,
+									adminUserId: uni.getStorageSync('ADMIN_USERID'),
+								},
+								success: (res) => {
+									console.info(res.data,'----扫码接口调用');
+									this.$app.storage.remove('ADMIN_USERID')
+								},
+								fail(err) {
+									console.log(err,'----错误信息');
+								}
+							});
+						}
 						this.$app.url.back()
 					});
 				}

+ 26 - 16
pages/my/my.vue

@@ -70,7 +70,10 @@
 						<image src="@/static/img/my-icon05.svg" class="icon"></image>
 						<view class="name">隐私条例</view>
 					</view>
-					
+<!-- 					<view class="act-item" @click="$app.url.goto('/pages/Invite-staff/Invite-staff')">
+						<image src="@/static/img/my-icon06.svg" class="icon"></image>
+						<view class="name">邀请员工</view>
+					</view> -->
 				</view>
 			</view>
 		</view>
@@ -120,24 +123,31 @@
 				});
 			},
 			getMyAccount(){
-				let channelUrl='http://192.168.2.21:9120/zs/channel/admin/'
+				let channelUrl='http://192.168.110.241:9120/zs/channel/admin/'
+				// let channelUrl='https://channel-api.zonelife.cn/zs/channel/admin/'
 				this.$api.base("post","/userApi/getUserAccount",{},{}).then(res=>{
 					this.userinfo = res.accountInfo
 					// 识别用户是否通过分销码进入小程序
-					uni.request({
-						url:channelUrl+'ums/umsAdminUser/distributionBindUser',
-						method: 'POST',
-						header: {
-							'content-type': 'application/json'
-						},
-						data: {
-							userId: uni.getStorageSync('USER_INFO').id,
-							adminUserId:parseInt(uni.getStorageSync('adminUserId')),
-						},
-						success: (res) => {
-							console.info('扫码接口调用');
-						}
-					});
+					if(uni.getStorageSync('ADMIN_USERID')){
+							uni.request({
+								url:channelUrl+'ums/umsAdminUser/distributionBindUser',
+								method: 'POST',
+								header: {
+									'content-type': 'application/json'
+								},
+								data: {
+									userId: uni.getStorageSync('USER_INFO').id,
+									adminUserId:parseInt(uni.getStorageSync('ADMIN_USERID')),
+								},
+								success: (res) => {
+									console.info(res.data,'----扫码接口调用');
+									this.$app.storage.remove('ADMIN_USERID')
+								},
+								fail(err) {
+									console.log(err,'----错误信息');
+								}
+							});
+					}
 				})
 			},
 			onGetPhoneNumber(e){

+ 36 - 0
pages/site-more/site-more.css

@@ -295,4 +295,40 @@
 	border-radius: 8px;
 	font-size: 14px;
 	background: linear-gradient(to right,#8FF8FB,#47AEFF);
+}
+.operation-price-box{
+	display: flex;
+	align-items: center;
+}
+.operation-symbol{
+	font-size: 24rpx;
+}
+.operation-price{
+	font-weight: 800;
+	font-size: 40rpx;
+	color: #F5531A;
+}
+.mini-text {
+	margin-left: 6rpx;
+	font-size: 12px;
+}
+
+.operation-price-btn {
+	margin-left: 20rpx;
+	width: 140rpx;
+	height: 40rpx;
+	background: #FF6457;
+	border-radius: 22rpx 22rpx 22rpx 22rpx;
+	font-style: italic; 
+	text-align: center;
+	line-height: 40rpx;
+	font-weight: 400;
+	font-size: 22rpx;
+	color: #FFFFFF;
+}
+.ordinary-price {
+	margin-left: 12rpx;
+	font-size: 24rpx;
+	color: #AAAAAA;
+	text-decoration: line-through;
 }

+ 13 - 1
pages/site-more/site-more.vue

@@ -43,10 +43,21 @@
 						<view class="value">{{item.time}}</view>
 					</view>
 					<view class="info">
-						<view class="row">
+						<view class="row" v-if="!isOperation">
 							<view class="name">抵扣券电价</view>
 							<view ><text class="value" >{{(item.price).toFixed(4)}}</text><text class="unit" >{{item.unit}}</text></view>
 						</view>
+						<view class="operation-price-box" v-else>
+							<view class="operation-price">
+								<text class="operation-symbol">¥</text>
+								<text>1.2099</text>
+							</view>
+							<view class="mini-text">元/度</view>
+							<view class="operation-price-btn">
+								企业专享价
+							</view>
+							<view class="ordinary-price">{{(item.price).toFixed(4)}}</view>
+						</view>
 						<!-- <view class="row">
 							<view class="name">服务费</view>
 							<view><text class="value">{{(item.addServicePrice+item.servicePrice).toFixed(4)}}</text><text class="unit">{{item.unit}}</text></view>
@@ -82,6 +93,7 @@
 		},
 		data() {
 			return {
+				isOperation:false,
 				another: false,
 				listHeight: 0,
 				prices:{

+ 32 - 0
pages/site/site.css

@@ -334,4 +334,36 @@ ax-body >>> .ax-body{
 	color: #2B303A;
 	background-image: linear-gradient(to right,#8FF8FB,#47AEFF);
 	margin: 0 10px;
+}
+.operation-symbol{
+	font-size: 24rpx;
+}
+.operation-price{
+	font-weight: 800;
+	font-size: 40rpx;
+	color: #F5531A;
+}
+.mini-text {
+	margin-left: 6rpx;
+	font-size: 12px;
+}
+
+.operation-price-btn {
+	margin-left: 20rpx;
+	width: 140rpx;
+	height: 40rpx;
+	background: #FF6457;
+	border-radius: 22rpx 22rpx 22rpx 22rpx;
+	font-style: italic; 
+	text-align: center;
+	line-height: 40rpx;
+	font-weight: 400;
+	font-size: 22rpx;
+	color: #FFFFFF;
+}
+.ordinary-price {
+	margin-left: 12rpx;
+	font-size: 24rpx;
+	color: #AAAAAA;
+	text-decoration: line-through;
 }

+ 13 - 1
pages/site/site.vue

@@ -42,13 +42,24 @@
 						<view class="text">当前价</view>
 						<image src="@/static/img/site-bg05.svg" class="bg" mode="heightFix"></image>
 					</view>
-					<view class="price-wrap">
+					<view class="price-wrap" v-if="!isOperation">
 						<view class="price">
 							<text class="symbol">¥</text>
 							<text>{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}}</text>
 						</view>
 						<view class="unit">元/度</view>
 					</view>
+					<view class="price-wrap" v-else>
+						<view class="operation-price">
+							<text class="operation-symbol">¥</text>
+							<text>{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}}</text>
+						</view>
+						<view class="mini-text">元/度</view>
+						<view class="operation-price-btn">
+							企业专享价
+						</view>
+						<view class="ordinary-price">{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}}</view>
+					</view>
 				</view>
 			</view>
 			<!-- 充电终端 -->
@@ -127,6 +138,7 @@
 		},
 		data() {
 			return {
+				isOperation:false,
 				mainHeight: 0,
 				tops:["../../static/img/$temp-site.png"],
 				another: false,

+ 41 - 1
pages/terminal/terminal.css

@@ -35,7 +35,7 @@
 	margin: 20px 0 10px;
 }
 .host-graph .image{
-	width: 60vw;
+	width: 40vw;
 }
 
 /* 主参数 */
@@ -272,4 +272,44 @@
 	border-radius: 8px;
 	height: 50px;
 	background: linear-gradient(to right,#8FF8FB,#47AEFF);
+}
+.coupon-image-card{
+	display: flex;
+	align-items: center;
+	margin-top: 28rpx;
+	width: 664rpx;
+	height: 160rpx;
+	/* background-image: url('@/static/img/coupon-gr.png'); */
+	background-repeat: no-repeat;
+	background-size: cover;
+	background-position: center;
+}
+.coupon-title{
+	width:220rpx;
+	text-align: center;
+	font-weight: bold;
+	font-size: 32rpx;
+	color: #FFFFFF;
+}
+.price-info{
+	width: 270rpx;
+	text-align: center;
+	color: #222222;
+}
+.price-numer{
+	font-weight: 800;
+	font-size: 48rpx;
+}
+.price-text{
+	font-size: 22rpx;
+}
+.shop-coupon-btn{
+	margin-left: 14rpx;
+	width: 136rpx;
+	height: 44rpx;
+	border-radius: 22rpx;
+	font-size: 24rpx;
+	color: #FFFFFF;
+	text-align: center;
+	line-height: 44rpx;
 }

+ 395 - 322
pages/terminal/terminal.vue

@@ -8,32 +8,66 @@
 			</view>
 			<!-- 主参数 -->
 			<view class="parameter">
-				<view class="param"><view class="value">{{deviceInfo.current}}</view><view class="name">电流A</view></view>
-				<view class="param"><view class="value">{{getVolt()}}</view><view class="name">电压V</view></view>
-				<view class="param"><view class="value">{{deviceInfo.power}}</view><view class="name">功率KW</view></view>
+				<view class="param">
+					<view class="value">{{deviceInfo.current}}</view>
+					<view class="name">电流A</view>
+				</view>
+				<view class="param">
+					<view class="value">{{getVolt()}}</view>
+					<view class="name">电压V</view>
+				</view>
+				<view class="param">
+					<view class="value">{{deviceInfo.power}}</view>
+					<view class="name">功率KW</view>
+				</view>
 			</view>
-			<view class="block">
+			<view v-if="!visit" class="block">
 				<view class="card">
-					<view class="title">终端信息</view>
-					<view class="cell"><view class="lable">终端状态</view><view class="contet">{{getDeviceStatusLable(deviceInfo.deviceStatus)}}</view></view>
-					<view class="cell"><view class="lable">终端编号</view><view class="contet">{{deviceInfo.deviceNo}}</view></view>
-					<view class="cell"><view class="lable">充电电站</view><view class="contet">{{deviceInfo.thirdPartyStationName}}</view></view>
-					<view class="cell"><view class="lable">充电终端</view><view class="contet">{{deviceInfo.deviceName}}</view></view>
-					<view class="cell"><view class="lable">车位编号</view><view class="contet">{{deviceInfo.parkNo ? deviceInfo.parkNo : "无"}}</view></view>
+					<view class="title">
+						<text>我的抵扣券</text>
+						<view class="more" v-if="isEc">
+							<view class="switch" :class="{'personal':personal==1}"><text
+									@click="changeAccount(1)">个人</text><text @click="changeAccount(2)">集团</text></view>
+						</view>
+					</view>
+					<view class="coupon-image-card"
+						:style="{backgroundImage:`url(${personal==1?selectedImg:defaultImg})`}">
+						<view class="coupon-title">充电抵扣券</view>
+						<view class="price-info">
+							<view class="price-numer">{{accountInfo.balance}}</view>
+							<view class="price-text">剩余可抵扣充电余量</view>
+						</view>
+						<view class="shop-coupon-btn" :style="{backgroundColor: personal==1?'#FF6457':'#3EB6F8'}" @click="gotoCouponBuy">立即购券</view>
+					</view>
+					<!-- 					<view class="coupon" :class="{'personal':personal==1}">
+						<view class="name">{{personal == 1?'充电抵扣券':'集团抵扣券'}}</view>
+						<view class="feature"><view class="line"></view></view>
+						<view class="info">
+							<view class="value">{{accountInfo.balance}}</view>
+							<view class="describe">剩余可抵扣充电费用 (元)</view>
+						</view>
+					</view> -->
 				</view>
 			</view>
 			<view class="block">
 				<view class="card">
 					<view class="title">
 						<text>费用信息</text>
-						<view @click="$app.url.goto('/pages/site-more/site-more?show=1&stationId='+stationInfo.id)" class="more"><text>价格详情</text><icon class="ax-iconline i-arrow-right icon"></icon></view>
+						<view @click="$app.url.goto('/pages/site-more/site-more?show=1&stationId='+stationInfo.id)"
+							class="more"><text>价格详情</text>
+							<icon class="ax-iconline i-arrow-right icon"></icon>
+						</view>
 					</view>
 					<view class="cell">
 						<view class="lable">{{personal == 1?'当前电价':'集团折扣价'}}</view>
 						<view class="contet app-flex middle">
-							<view v-if="personal == 1"><text class="money">{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}}</text><text> 元/度</text></view>
+							<view v-if="personal == 1"><text
+									class="money">{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}}</text><text>
+									元/度</text></view>
 							<view v-else>
-								<text class="obsolete"> {{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}} 元/度 </text>
+								<text class="obsolete">
+									{{nowPriceTime.price?parseFloat(nowPriceTime.price).toFixed(4):"0.0000"}} 元/度
+								</text>
 								<text class="money">{{getCurrEcPrice()}}</text><text> 元/度</text>
 							</view>
 						</view>
@@ -41,31 +75,44 @@
 					<view class="cell" v-if="discountInfo&&personal == 1">
 						<view class="lable">优惠</view>
 						<view class="contet app-flex middle">
-							<view>{{discountInfo.temp3}}<text class="money">{{discountInfo.discount?parseFloat(discountInfo.discount).toFixed(4):"0.0000"}}</text><text> 元/度</text></view>
+							<view>{{discountInfo.temp3}}<text
+									class="money">{{discountInfo.discount?parseFloat(discountInfo.discount).toFixed(4):"0.0000"}}</text><text>
+									元/度</text></view>
 						</view>
 					</view>
-					<view class="cell"><view class="lable">当前时段</view><view class="contet">{{getPriceLable(nowPriceTime.timeType)}}  {{nowPriceTime.time}}</view></view>
+					<view class="cell">
+						<view class="lable">当前时段</view>
+						<view class="contet">{{getPriceLable(nowPriceTime.timeType)}} {{nowPriceTime.time}}</view>
+					</view>
 					<!-- <view class="cell" style="height: auto;"><view class="lable">停车参考</view><view class="contet" style="flex: 1;padding-left: 5px;" v-html="stationInfo.parkTips"></view></view> -->
-					<view class="cell"><view class="lable">停车参考</view><view class="contet">充电减免2小时停车费,超出时长部分计时收费</view></view>
+					<view class="cell">
+						<view class="lable">停车参考</view>
+						<view class="contet">充电减免2小时停车费,超出时长部分计时收费</view>
+					</view>
 				</view>
 			</view>
-			<view v-if="!visit" class="block">
+			<view class="block">
 				<view class="card">
-					<view class="title">
-						<text>我的抵扣券</text>
-						<view class="more" v-if="isEc"><view  class="switch" :class="{'personal':personal==1}"><text @click="changeAccount(1)">个人</text><text  @click="changeAccount(2)">集团</text></view></view>
+					<view class="title">终端信息</view>
+					<view class="cell">
+						<view class="lable">终端状态</view>
+						<view class="contet">{{getDeviceStatusLable(deviceInfo.deviceStatus)}}</view>
 					</view>
-					<view class="coupon" :class="{'personal':personal==1}">
-						<view class="name">{{personal == 1?'充电抵扣券':'集团抵扣券'}}</view>
-						<view class="feature"><view class="line"></view></view>
-						<view class="info">
-							<view class="value">{{accountInfo.balance}}</view>
-							<view class="describe">剩余可抵扣充电费用 (元)</view>
-						</view>
+					<view class="cell">
+						<view class="lable">终端编号</view>
+						<view class="contet">{{deviceInfo.deviceNo}}</view>
 					</view>
-					<view class="tips">
-						<view class="li">1. 可抵扣按照当前电价进行预估计算</view>
-						<view class="li">2. 实际结算价以具体充电时段为准</view>
+					<view class="cell">
+						<view class="lable">充电电站</view>
+						<view class="contet">{{deviceInfo.thirdPartyStationName}}</view>
+					</view>
+					<view class="cell">
+						<view class="lable">充电终端</view>
+						<view class="contet">{{deviceInfo.deviceName}}</view>
+					</view>
+					<view class="cell">
+						<view class="lable">车位编号</view>
+						<view class="contet">{{deviceInfo.parkNo ? deviceInfo.parkNo : "无"}}</view>
 					</view>
 				</view>
 			</view>
@@ -82,319 +129,345 @@
 </template>
 
 <script>
-export default {
-	onLoad(opts) {
-		console.log("参数信息:",opts)
-	   /**
-		* 判断终端是否占用状态
-		* 判断占用终端设备的是不是用户自己
-		*/
-		this.deviceId = opts.deviceId
-		this.deviceStatus = opts.deviceStatus
-	   
-	   
-	},
-	onShow() {
-		
-		this.queryInChange(this.deviceId,this.deviceStatus);
-		
-		/**
-		 * 刷新用户信息
-		 */
-		this.userInfo = this.$app.storage.get(this.$config.keyname.userInfo);
-		if(!this.userInfo.phone){
-			this.$app.url.goto('/pages/login/login')
-			return;
-		}
-		this.$api.login({"checkStatus":1}).then(()=>{
+	export default {
+		onLoad(opts) {
+			console.log("参数信息:", opts)
+			/**
+			 * 判断终端是否占用状态
+			 * 判断占用终端设备的是不是用户自己
+			 */
+			this.deviceId = opts.deviceId
+			this.deviceStatus = opts.deviceStatus
+
+
+		},
+		onShow() {
+
+			this.queryInChange(this.deviceId, this.deviceStatus);
+
+			/**
+			 * 刷新用户信息
+			 */
 			this.userInfo = this.$app.storage.get(this.$config.keyname.userInfo);
-			if(this.userInfo.ecId){
-				//查询该集团账户是否正常使用。
-				this.$api.base("post","/chargeApi/queryEcInfo",{"ecId":this.userInfo.ecId},{}).then(res=>{
-					if(res.ecInfo && res.ecInfo.ecStatus == 1){
-						this.isEc = true;
-					}
-				})
-			}
-		})
-		
-		
-		
-	},
-	mounted(){
-		
-		
-	},
-	data() {
-		return {
-			deviceId:0,
-			deviceStatus:0,
-			visit: '',
-			personal: 1,// 1 个人订单 2 集团订单
-			isEc : false,//是否集团的用户
-			nowPriceTime : {},//当前价格时间段信息
-			deviceInfo : {},//充电桩的信息
-			accountInfo : {//账户信息
-				balance : 0,//可用抵用券余额
-			},
-			userInfo : {},
-			stationInfo : {},//站点信息
-			orderInfo : {},//临时订单信息
-			checkNum : 0,//检测订单状态次数
-			ecInfo : {},//集团信息
-			discountInfo:null//优惠信息
-		}
-	},
-	methods: {
-		//通过用户id查询是否还有在充电中的订单
-		queryInChange(deviceId,deviceStatus){
-			if(deviceStatus == 3 || deviceStatus == 4 ){
-				//占用充电状态;终端占用且不是自己,进入访问模式
-				this.visit = true;
+			if (!this.userInfo.phone) {
+				this.$app.url.goto('/pages/login/login')
+				return;
 			}
-			this.getDeviceInfo(deviceId);//获取设备、站的详情信息
-			this.getAccountInfo();//获取账户信息
-			/* this.$api.base("post","/chargeApi/queryInChangeByUserId",{},{}).then(res=>{
-				if(res.isChange == 1){
-					this.orderInfo = res.orderInfo;
-					//用户有充电中的订单
-					//if(res.orderInfo.deviceId == deviceId){
-						// 跳转
-						this.$app.url.goto('/pages/charging/charging?orderId='+this.orderInfo.id+"&deviceId="+deviceId,false);
-					//}
-				}else{
-					
-				}
-				
-				
-			}) */
-		},
-		//获取设备的详情信息
-		getDeviceInfo(deviceId){
-			this.$api.base("post","/chargeApi/getDevicesDetial",{"deviceId":deviceId},{}).then(res=>{
-				this.deviceInfo = res.device;
-				this.nowPriceTime = res.nowPriceTime;
-				this.stationInfo = res.stationInfo;
-				if(res.discountInfo){
-					this.discountInfo = res.discountInfo
-				}
-				if(this.deviceInfo.tipsStatus==1){
-					this.$app.popup.alert(this.deviceInfo.tipsContent,"温馨提示");
+			this.$api.login({
+				"checkStatus": 1
+			}).then(() => {
+				this.userInfo = this.$app.storage.get(this.$config.keyname.userInfo);
+				if (this.userInfo.ecId) {
+					//查询该集团账户是否正常使用。
+					this.$api.base("post", "/chargeApi/queryEcInfo", {
+						"ecId": this.userInfo.ecId
+					}, {}).then(res => {
+						if (res.ecInfo && res.ecInfo.ecStatus == 1) {
+							this.isEc = true;
+						}
+					})
 				}
 			})
+
+
+
 		},
-		//获取账户信息
-		getAccountInfo(){
-			if(this.personal == 1){
-				//获取个人账户信息
-				this.$api.base("post","/chargeApi/getUserAccount",{},{}).then(res=>{
-					this.accountInfo.balance = res.userAccount.balance;
-					
-				})
-			}else{
-				//获取集团账户信息
-				this.$api.base("post","/chargeApi/getEcUserAccount",{},{error:false}).then(res=>{
-					this.ecInfo = res.ecInfo;
-					this.accountInfo.balance = res.ecUserAccount.balance;
-				}).catch(err=>{
-					this.accountInfo.balance = 0;
-					this.$app.popup.alert(err.msg,"温馨提示");
-				})
-			}
+		mounted() {
+
+
 		},
-		//切换账户
-		changeAccount(type){
-			if(this.personal == type){
-				return;
+		data() {
+			return {
+				deviceId: 0,
+				deviceStatus: 0,
+				visit: '',
+				personal: 1, // 1 个人订单 2 集团订单
+				isEc: false, //是否集团的用户
+				nowPriceTime: {}, //当前价格时间段信息
+				deviceInfo: {}, //充电桩的信息
+				accountInfo: { //账户信息
+					balance: 0, //可用抵用券余额
+				},
+				userInfo: {},
+				stationInfo: {}, //站点信息
+				orderInfo: {}, //临时订单信息
+				checkNum: 0, //检测订单状态次数
+				ecInfo: {}, //集团信息
+				discountInfo: null ,//优惠信息
+				selectedImg:'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/AwQTRxpEMqOG50293e26d86888b3e0f7324c429d2019.png/1.png',
+				defaultImg:'https://hyxhsh.oss-cn-chengdu.aliyuncs.com/63b7c68b71a69169d1b33f92/store/bdb/user/avatar/SPh1u3KAqte830a107b2e3c5033b1d1027516d84d780.png/1.png'
 			}
-			this.personal = type;
-			 this.getAccountInfo();
 		},
-		//当前集团折扣价
-		getCurrEcPrice(){
-			
-			if(!this.nowPriceTime){
-				return 0;
-			}
-			var currEcP = this.nowPriceTime.price;
-			if(this.ecInfo && this.ecInfo.ecDiscount){
-				
-				var realServicePrice = this.nowPriceTime.servicePrice
-				if(this.stationInfo.contractServicePrice){
-					realServicePrice = this.stationInfo.contractServicePrice
+		methods: {
+			//通过用户id查询是否还有在充电中的订单
+			queryInChange(deviceId, deviceStatus) {
+				if (deviceStatus == 3 || deviceStatus == 4) {
+					//占用充电状态;终端占用且不是自己,进入访问模式
+					this.visit = true;
 				}
-				
-				currEcP = (this.nowPriceTime.electrovalence + realServicePrice + this.nowPriceTime.addServicePrice * this.ecInfo.ecDiscount/100);
-			}
-			console.log("dddd",currEcP)
-			if(currEcP){
-				currEcP = currEcP.toFixed(2);
-			}
-			return Number(currEcP).toFixed(4);
-		},
-		//转换出电压值
-		getVolt(){
-			if(!this.deviceInfo.power){
-				return 0;
-			}
-			var v = this.deviceInfo.power/this.deviceInfo.current * 1000;
-			return v;
-		},
-		//映射 峰  平  谷
-		getPriceLable(type){
-			//时间类型 1 谷 2 平 3 峰
-			var str = "";
-			switch (type){
-				case 1:
-					str = "谷";
-					break;
-				case 2:
-					str = "平";
-					break;
-				case 3:
-					str = "峰";
-					break;
-			}
-			return str;
-		},
-		//映射订单状态名称()
-		getDeviceStatusLable(status){
-			//设备状态 0:离网1:空闲2:占用(未充电)3:占用(充电中)4:占用(预约锁定)255:故障
-			var str = "";
-			switch (status){
-				case 0:
-					str = "离网";
-					break;
-				case 1:
-					str = "空闲";
-					break;
-				case 2:
-					str = "占用";
-					break;
-				case 3:
-					str = "占用";
-					break;
-				case 4:
-					str = "占用";
-					break;
-				case 255:
-					str = "故障";
-					break;
-			}
-			return str;
-		},
-		startup(){
-			
-			if(this.visit){
-				this.$app.popup.alert("该充电枪被占用或存在异常,请重新尝试或更换其他充电枪。","温馨提示");
-				return;
-			}
-			
-			//判断账户余额是否大于两元
-			if((this.accountInfo.balance - 2) <= 0){
-				
-				return this.$app.popup.confirm("无法启动充电,抵扣余量需大于2元,请先购买充电券!","温馨提示!",{confirmText:"立即购券"}).then(confirm=>{
-					if(confirm){
-						this.$app.url.goto('/pages/coupon-buy/coupon-buy',true);
+				this.getDeviceInfo(deviceId); //获取设备、站的详情信息
+				this.getAccountInfo(); //获取账户信息
+				/* this.$api.base("post","/chargeApi/queryInChangeByUserId",{},{}).then(res=>{
+					if(res.isChange == 1){
+						this.orderInfo = res.orderInfo;
+						//用户有充电中的订单
+						//if(res.orderInfo.deviceId == deviceId){
+							// 跳转
+							this.$app.url.goto('/pages/charging/charging?orderId='+this.orderInfo.id+"&deviceId="+deviceId,false);
+						//}
+					}else{
+						
 					}
-				});
-			}
-			//统一下单并启动接口
-			this.startChangeAndOrder();
-		},
-		//统一下单并启动接口
-		startChangeAndOrder(){
-			var obj = {
-				userId : this.userInfo.id,
-				deviceId : this.deviceInfo.id,
-				orderType : this.personal == 2 ? 2 : 1,//订单类型 1 个人订单 2 集团订单
-			}
-			this.$api.base("post","/chargeApi/startChangeAndOrder",obj,{}).then(res=>{
-				//下单成功,并进行了订单预充值
-				this.orderInfo = res.orderInfo;
-				if(res.flg && res.flg == 1){
-					//用户有充电中的订单
-					 this.$app.popup.confirm("您有一个进行中充电订单,不可再次启动。",null,{showCancel:false,confirmText:"查看订单"}).then(cres=>{
-						this.$app.url.goto('/pages/charging/charging?orderId='+this.orderInfo.id+"&deviceId="+this.deviceInfo.id,false);
-					 });
-				}else{
-					//正常启动充电订单
-					this.$app.url.goto('/pages/charging/charging?orderId='+this.orderInfo.id+"&deviceId="+this.deviceInfo.id,false);
+					
+					
+				}) */
+			},
+			//获取设备的详情信息
+			getDeviceInfo(deviceId) {
+				this.$api.base("post", "/chargeApi/getDevicesDetial", {
+					"deviceId": deviceId
+				}, {}).then(res => {
+					this.deviceInfo = res.device;
+					this.nowPriceTime = res.nowPriceTime;
+					this.stationInfo = res.stationInfo;
+					if (res.discountInfo) {
+						this.discountInfo = res.discountInfo
+					}
+					if (this.deviceInfo.tipsStatus == 1) {
+						this.$app.popup.alert(this.deviceInfo.tipsContent, "温馨提示");
+					}
+				})
+			},
+			//获取账户信息
+			getAccountInfo() {
+				if (this.personal == 1) {
+					//获取个人账户信息
+					this.$api.base("post", "/chargeApi/getUserAccount", {}, {}).then(res => {
+						this.accountInfo.balance = res.userAccount.balance;
+
+					})
+				} else {
+					//获取集团账户信息
+					this.$api.base("post", "/chargeApi/getEcUserAccount", {}, {
+						error: false
+					}).then(res => {
+						this.ecInfo = res.ecInfo;
+						this.accountInfo.balance = res.ecUserAccount.balance;
+					}).catch(err => {
+						this.accountInfo.balance = 0;
+						this.$app.popup.alert(err.msg, "温馨提示");
+					})
 				}
-				//this.$app.popup.loading(true,{title:"启动中...."})
-				//延迟5s查询一下订单,看看是否真的启动成功
-				//setTimeout(()=>this.checkedStartStatus(),5000);
-			})	
-		},
-		/* // 通过充电桩编号(sn)检测该设备是否插枪,是否可以进行后续的下单,启动操作
-		checkDeviceReady(){
-			this.$api.base("post","/chargeApi/checkDeviceReady",{"sn":this.deviceInfo.deviceNo},{}).then(res=>{
-				if(res.code == 0){
-					//充电桩已经插枪准备好了,可以进行下单充值操作
-					this.chargeAndOrder();
+			},
+			//切换账户
+			changeAccount(type) {
+				if (this.personal == type) {
+					return;
 				}
-			})
-		},
-		// 进行下单,并进行接口充值,准备启动充电
-		chargeAndOrder(){
-			var obj = {
-				userId : this.userInfo.id,
-				deviceId : this.deviceInfo.id,
-				orderType : this.personal == 2 ? 2 : 1,//订单类型 1 个人订单 2 集团订单
-			}
-			this.$api.base("post","/chargeApi/chargeAndOrder",obj,{}).then(res=>{
-				if(res.code == 0){
+				this.personal = type;
+				this.getAccountInfo();
+			},
+			//当前集团折扣价
+			getCurrEcPrice() {
+
+				if (!this.nowPriceTime) {
+					return 0;
+				}
+				var currEcP = this.nowPriceTime.price;
+				if (this.ecInfo && this.ecInfo.ecDiscount) {
+
+					var realServicePrice = this.nowPriceTime.servicePrice
+					if (this.stationInfo.contractServicePrice) {
+						realServicePrice = this.stationInfo.contractServicePrice
+					}
+
+					currEcP = (this.nowPriceTime.electrovalence + realServicePrice + this.nowPriceTime.addServicePrice *
+						this.ecInfo.ecDiscount / 100);
+				}
+				console.log("dddd", currEcP)
+				if (currEcP) {
+					currEcP = currEcP.toFixed(2);
+				}
+				return Number(currEcP).toFixed(4);
+			},
+			//转换出电压值
+			getVolt() {
+				if (!this.deviceInfo.power) {
+					return 0;
+				}
+				var v = this.deviceInfo.power / this.deviceInfo.current * 1000;
+				return v;
+			},
+			//映射 峰  平  谷
+			getPriceLable(type) {
+				//时间类型 1 谷 2 平 3 峰
+				var str = "";
+				switch (type) {
+					case 1:
+						str = "谷";
+						break;
+					case 2:
+						str = "平";
+						break;
+					case 3:
+						str = "峰";
+						break;
+				}
+				return str;
+			},
+			//映射订单状态名称()
+			getDeviceStatusLable(status) {
+				//设备状态 0:离网1:空闲2:占用(未充电)3:占用(充电中)4:占用(预约锁定)255:故障
+				var str = "";
+				switch (status) {
+					case 0:
+						str = "离网";
+						break;
+					case 1:
+						str = "空闲";
+						break;
+					case 2:
+						str = "占用";
+						break;
+					case 3:
+						str = "占用";
+						break;
+					case 4:
+						str = "占用";
+						break;
+					case 255:
+						str = "故障";
+						break;
+				}
+				return str;
+			},
+			startup() {
+
+				if (this.visit) {
+					this.$app.popup.alert("该充电枪被占用或存在异常,请重新尝试或更换其他充电枪。", "温馨提示");
+					return;
+				}
+
+				//判断账户余额是否大于两元
+				if ((this.accountInfo.balance - 3) <= 0) {
+
+					return this.$app.popup.confirm("无法启动充电,抵扣余量需大于3元,请先购买充电券!", "温馨提示!", {
+						confirmText: "立即购券"
+					}).then(confirm => {
+						if (confirm) {
+							this.$app.url.goto('/pages/coupon-buy/coupon-buy', true);
+						}
+					});
+				}
+				//统一下单并启动接口
+				this.startChangeAndOrder();
+			},
+			
+			// 主动发起购券操作
+			gotoCouponBuy(){
+				this.$app.url.goto('/pages/coupon-buy/coupon-buy', true);
+			},
+			//统一下单并启动接口
+			startChangeAndOrder() {
+				var obj = {
+					userId: this.userInfo.id,
+					deviceId: this.deviceInfo.id,
+					orderType: this.personal == 2 ? 2 : 1, //订单类型 1 个人订单 2 集团订单
+				}
+				this.$api.base("post", "/chargeApi/startChangeAndOrder", obj, {}).then(res => {
 					//下单成功,并进行了订单预充值
 					this.orderInfo = res.orderInfo;
-					//通知已经充值成功,可以进行设备充电的启动
-					this.changePayAndStart();
-				}
-			})
-		},
-		// 支付成功启动充电通知
-		changePayAndStart(){
-			this.$api.base("post","/chargeApi/changePayAndStart",{"id":this.orderInfo.id},{}).then(res=>{
-				if(res.code == 0){
-					this.$app.popup.loading(ture,{title:"启动中...."})
+					if (res.flg && res.flg == 1) {
+						//用户有充电中的订单
+						this.$app.popup.confirm("您有一个进行中充电订单,不可再次启动。", null, {
+							showCancel: false,
+							confirmText: "查看订单"
+						}).then(cres => {
+							this.$app.url.goto('/pages/charging/charging?orderId=' + this.orderInfo.id +
+								"&deviceId=" + this.deviceInfo.id, false);
+						});
+					} else {
+						//正常启动充电订单
+						this.$app.url.goto('/pages/charging/charging?orderId=' + this.orderInfo.id + "&deviceId=" +
+							this.deviceInfo.id, false);
+					}
+					//this.$app.popup.loading(true,{title:"启动中...."})
 					//延迟5s查询一下订单,看看是否真的启动成功
-					setTimeout(()=>this.checkedStartStatus(),5000);
+					//setTimeout(()=>this.checkedStartStatus(),5000);
+				})
+			},
+			/* // 通过充电桩编号(sn)检测该设备是否插枪,是否可以进行后续的下单,启动操作
+			checkDeviceReady(){
+				this.$api.base("post","/chargeApi/checkDeviceReady",{"sn":this.deviceInfo.deviceNo},{}).then(res=>{
+					if(res.code == 0){
+						//充电桩已经插枪准备好了,可以进行下单充值操作
+						this.chargeAndOrder();
+					}
+				})
+			},
+			// 进行下单,并进行接口充值,准备启动充电
+			chargeAndOrder(){
+				var obj = {
+					userId : this.userInfo.id,
+					deviceId : this.deviceInfo.id,
+					orderType : this.personal == 2 ? 2 : 1,//订单类型 1 个人订单 2 集团订单
 				}
-			})
-		}, */
-		// 延迟5s查询一下订单,看看是否真的启动成功
-		checkedStartStatus(){
-			this.$api.base("post","/chargeApi/checkedStartStatus",{"id":this.orderInfo.id},{}).then(res=>{
-				
-				if(res.code == 0){
-					var respObj = res.obj;
-					if(respObj.code == 200){
-						if(respObj.status == 1){
-							uni.hideLoading();
-							//状态为1说明正常启动
-							this.$app.url.goto('/pages/charging/charging?orderId='+this.orderInfo.id+"&deviceId="+this.deviceInfo.id,false);
-						}else{
-							//其他状态,说明充电桩,未启动。或其他问题,需要再次进行进行验证
-							if(this.checkNum < 20){
-								this.checkNum = this.checkNum + 1;
-								//延迟2s查询一下订单,看看是否真的启动成功
-								setTimeout(()=>this.checkedStartStatus(),2000);
+				this.$api.base("post","/chargeApi/chargeAndOrder",obj,{}).then(res=>{
+					if(res.code == 0){
+						//下单成功,并进行了订单预充值
+						this.orderInfo = res.orderInfo;
+						//通知已经充值成功,可以进行设备充电的启动
+						this.changePayAndStart();
+					}
+				})
+			},
+			// 支付成功启动充电通知
+			changePayAndStart(){
+				this.$api.base("post","/chargeApi/changePayAndStart",{"id":this.orderInfo.id},{}).then(res=>{
+					if(res.code == 0){
+						this.$app.popup.loading(ture,{title:"启动中...."})
+						//延迟5s查询一下订单,看看是否真的启动成功
+						setTimeout(()=>this.checkedStartStatus(),5000);
+					}
+				})
+			}, */
+			// 延迟5s查询一下订单,看看是否真的启动成功
+			checkedStartStatus() {
+				this.$api.base("post", "/chargeApi/checkedStartStatus", {
+					"id": this.orderInfo.id
+				}, {}).then(res => {
+
+					if (res.code == 0) {
+						var respObj = res.obj;
+						if (respObj.code == 200) {
+							if (respObj.status == 1) {
+								uni.hideLoading();
+								//状态为1说明正常启动
+								this.$app.url.goto('/pages/charging/charging?orderId=' + this.orderInfo.id +
+									"&deviceId=" + this.deviceInfo.id, false);
+							} else {
+								//其他状态,说明充电桩,未启动。或其他问题,需要再次进行进行验证
+								if (this.checkNum < 20) {
+									this.checkNum = this.checkNum + 1;
+									//延迟2s查询一下订单,看看是否真的启动成功
+									setTimeout(() => this.checkedStartStatus(), 2000);
+								}
 							}
+
+						} else {
+							uni.hideLoading();
+							this.$app.popup.alert(respObj.msg, "温馨提示");
 						}
-						
-					}else{
+
+					} else {
 						uni.hideLoading();
-						this.$app.popup.alert(respObj.msg,"温馨提示");
 					}
-					
-				}else{
-					uni.hideLoading();
-				}
-			})
-		},
+				})
+			},
+		}
 	}
-}
 </script>
 
 <style scoped>
-@import url("terminal.css");
-</style>
+	@import url("terminal.css");
+</style>

二進制
static/img/coupon-gr.png


二進制
static/img/coupon-group.png


+ 1 - 0
static/img/my-icon06.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30.25" height="30" viewBox="0 0 30.25 30"><defs><style>.a{fill:none;}.b{fill:#2b303a;stroke:#2b303a;stroke-width:0.5px;}.c{fill:#8ef7fb;}</style></defs><g transform="translate(-214.75 -515)"><rect class="a" width="30" height="30" transform="translate(215 515)"/><g transform="translate(0.5 0.5)"><path class="b" d="M88.5,83.654a.928.928,0,0,0,1.305-.305,1.129,1.129,0,0,0,.153-.687,1.66,1.66,0,0,0-.384-.61,11.878,11.878,0,0,0-1.689-.916,7.441,7.441,0,0,0,1.766-2.595,8.569,8.569,0,1,0-16.43-3.358,8.463,8.463,0,0,0,2.534,6.029,14.032,14.032,0,0,0-8.215,12.744.921.921,0,0,0,1.842,0A12.238,12.238,0,0,1,77.37,82.509a8.748,8.748,0,0,0,8.752,0,11.4,11.4,0,0,1,2.38,1.145Zm-6.756-1.832a6.639,6.639,0,1,1,6.68-6.639,6.392,6.392,0,0,1-.537,2.671,6.329,6.329,0,0,1-1.459,2.06,6.641,6.641,0,0,1-4.683,1.908Z" transform="translate(146.956 448.939)"/><g transform="translate(1 1)"><rect class="c" width="2.4" height="10" rx="1.2" transform="translate(237 532)"/><rect class="c" width="2.4" height="10" rx="1.2" transform="translate(243 536) rotate(90)"/></g></g></g></svg>

二進制
static/img/share.jpg


+ 1 - 0
static/img/tips-icon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:#ffec3e;}.b{fill:#48afff;}.c{fill:none;}</style></defs><g transform="translate(-97 -556)"><g transform="translate(36.988 477.398)"><path class="a" d="M393.611,757.6l-2.032,1.962a.673.673,0,0,1-1.113-.293l-1.039-3.506Z" transform="translate(-320.455 -658.796)"/><path class="b" d="M61.545,91.879a1.044,1.044,0,0,0,.089,1.867l7.381,3.24,9.745,4.279a1.044,1.044,0,0,0,1.452-.8l2.8-18.662a1.044,1.044,0,0,0-1.541-1.066Z" transform="translate(0)"/><path class="a" d="M337.313,227.673l1.118.491,10.355-11.5a1.274,1.274,0,0,0-1.894-1.705l-10.919,12.128,1.339.588Z" transform="translate(-268.298 -130.687)"/></g><rect class="c" width="24" height="24" transform="translate(97 556)"/></g></svg>

+ 0 - 0
unpackage/dist/build/.automator/mp-weixin/.automator.json


+ 4 - 0
unpackage/dist/build/mp-weixin/app.js

@@ -0,0 +1,4 @@
+
+require('./common/runtime.js')
+require('./common/vendor.js')
+require('./common/main.js')

+ 42 - 0
unpackage/dist/build/mp-weixin/app.json

@@ -0,0 +1,42 @@
+{
+  "pages": [
+    "pages/index/index",
+    "pages/map/map",
+    "pages/my/my",
+    "pages/site/site",
+    "pages/site-more/site-more",
+    "pages/charging/charging",
+    "pages/order-detail/order-detail",
+    "pages/terminal/terminal",
+    "pages/coupon-buy/coupon-buy",
+    "pages/login/login",
+    "pages/recharge-log/recharge-log",
+    "pages/feedback/feedback",
+    "pages/order/order",
+    "pages/search/search",
+    "pages/Invite-staff/Invite-staff",
+    "pages/web/web",
+    "pages/feedback-reply/feedback-reply"
+  ],
+  "subPackages": [],
+  "window": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "中数电动",
+    "navigationBarBackgroundColor": "#C7FFFD",
+    "backgroundColor": "#C7FFFD",
+    "navigationStyle": "custom"
+  },
+  "permission": {
+    "scope.userFuzzyLocation": {
+      "desc": "提供周边线下服务商"
+    },
+    "scope.userLocation": {
+      "desc": "提供周边线下服务商"
+    }
+  },
+  "requiredPrivateInfos": [
+    "getLocation"
+  ],
+  "lazyCodeLoading": "requiredComponents",
+  "usingComponents": {}
+}

+ 3 - 0
unpackage/dist/build/mp-weixin/app.wxss

@@ -0,0 +1,3 @@
+@import './common/main.wxss';
+
+[data-custom-hidden="true"],[bind-data-custom-hidden="true"]{display: none !important;}

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/common/main.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/common/main.wxss


File diff suppressed because it is too large
+ 2 - 0
unpackage/dist/build/mp-weixin/common/runtime.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/common/vendor.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.js


+ 6 - 0
unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.json

@@ -0,0 +1,6 @@
+{
+  "usingComponents": {
+    "ax-ios-indicator": "/components/ax-ios-indicator/ax-ios-indicator"
+  },
+  "component": true
+}

+ 1 - 0
unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.wxml

@@ -0,0 +1 @@
+<view class="app-navigation data-v-3e1fb9a0"><view class="__body data-v-3e1fb9a0"><view data-event-opts="{{[['tap',[['act',['home']]]]]}}" class="nav-item data-v-3e1fb9a0" bindtap="__e"><image class="icon data-v-3e1fb9a0" src="{{homeIcon}}"></image><view class="name data-v-3e1fb9a0">首页</view></view><view data-event-opts="{{[['tap',[['sacn']]]]}}" class="scan data-v-3e1fb9a0" bindtap="__e"><image class="icon-scan data-v-3e1fb9a0" src="/static/img/appnav-scan.svg.svg"></image></view><view class="scan-placeholder data-v-3e1fb9a0"></view><view data-event-opts="{{[['tap',[['act',['my']]]]]}}" class="nav-item data-v-3e1fb9a0" bindtap="__e"><image class="icon data-v-3e1fb9a0" src="{{myIcon}}"></image><view class="name data-v-3e1fb9a0">我的</view></view></view><ax-ios-indicator vue-id="e37c8b7c-1" min="10" class="data-v-3e1fb9a0" bind:__l="__l"></ax-ios-indicator></view>

+ 1 - 0
unpackage/dist/build/mp-weixin/components/app-navigation/app-navigation.wxss

@@ -0,0 +1 @@
+.app-navigation.data-v-3e1fb9a0{background-color:#fff;border-radius:15px 15px 0 0;-webkit-filter:drop-shadow(0 -3px 6px rgba(0,0,0,.05));filter:drop-shadow(0 -3px 6px rgba(0,0,0,.05))}.app-navigation .__body.data-v-3e1fb9a0{display:flex;align-items:center;justify-content:space-around;position:relative;padding:10px;padding-bottom:0}.scan.data-v-3e1fb9a0{display:inline-flex;align-items:center;justify-content:center;width:60px;height:60px;border-radius:100pc;background-image:linear-gradient(90deg,#8ff8fb,#47aeff);box-shadow:0 3px 6px #00bfe1 inset;border:3px solid #fff;position:absolute;-webkit-transform:translateY(-10px);transform:translateY(-10px)}.scan > .icon-scan.data-v-3e1fb9a0{display:block;width:22.5px;height:22.5px}.scan-placeholder.data-v-3e1fb9a0{width:60px}.nav-item > .name.data-v-3e1fb9a0{font-size:10px;margin-top:4px}.nav-item > .icon.data-v-3e1fb9a0{display:block;width:22px;height:22px}

+ 10 - 0
unpackage/dist/build/mp-weixin/components/ax-body/ax-body.js

@@ -0,0 +1,10 @@
+(global["webpackJsonp"]=global["webpackJsonp"]||[]).push([["components/ax-body/ax-body"],{"805c":function(t,n,e){},"881f":function(t,n,e){"use strict";e.d(n,"b",(function(){return i})),e.d(n,"c",(function(){return u})),e.d(n,"a",(function(){return o}));var o={axCustomTitle:function(){return Promise.all([e.e("common/vendor"),e.e("components/ax-custom-title/ax-custom-title")]).then(e.bind(null,"6da2"))},axIosIndicator:function(){return e.e("components/ax-ios-indicator/ax-ios-indicator").then(e.bind(null,"2348"))}},i=function(){var t=this.$createElement,n=(this._self._c,this.__get_style([this.StyleSheet]));this.$mp.data=Object.assign({},{$root:{s0:n}})},u=[]},8839:function(t,n,e){"use strict";e.r(n);var o=e("881f"),i=e("b496");for(var u in i)["default"].indexOf(u)<0&&function(t){e.d(n,t,(function(){return i[t]}))}(u);e("d169");var a=e("828b"),c=Object(a["a"])(i["default"],o["b"],o["c"],!1,null,null,null,!1,o["a"],void 0);n["default"]=c.exports},b496:function(t,n,e){"use strict";e.r(n);var o=e("eb2d"),i=e.n(o);for(var u in o)["default"].indexOf(u)<0&&function(t){e.d(n,t,(function(){return o[t]}))}(u);n["default"]=i.a},d169:function(t,n,e){"use strict";var o=e("805c"),i=e.n(o);i.a},eb2d:function(t,n,e){"use strict";(function(t){Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;var e={name:"ax-body",props:{blank:{type:[Number,String],default:10},hideIndicatorArea:{type:Boolean,default:!1}},mounted:function(){var n=this;t.createSelectorQuery().in(this).select(".__root").boundingClientRect((function(t){t&&n.$emit("init",t)})).exec()},computed:{StyleSheet:function(){return{"--blank":"".concat(Number(this.blank)||0,"px")}}}};n.default=e}).call(this,e("df3c")["default"])}}]);
+;(global["webpackJsonp"] = global["webpackJsonp"] || []).push([
+    'components/ax-body/ax-body-create-component',
+    {
+        'components/ax-body/ax-body-create-component':(function(module, exports, __webpack_require__){
+            __webpack_require__('df3c')['createComponent'](__webpack_require__("8839"))
+        })
+    },
+    [['components/ax-body/ax-body-create-component']]
+]);

+ 7 - 0
unpackage/dist/build/mp-weixin/components/ax-body/ax-body.json

@@ -0,0 +1,7 @@
+{
+  "usingComponents": {
+    "ax-custom-title": "/components/ax-custom-title/ax-custom-title",
+    "ax-ios-indicator": "/components/ax-ios-indicator/ax-ios-indicator"
+  },
+  "component": true
+}

+ 1 - 0
unpackage/dist/build/mp-weixin/components/ax-body/ax-body.wxml

@@ -0,0 +1 @@
+<view class="ax ax-body" style="{{$root.s0}}"><block wx:if="{{$slots.title}}"><ax-custom-title bind:display="__e" vue-id="2301dcd0-1" data-event-opts="{{[['^display',[['$emit',['display','$event']]]]]}}" bind:__l="__l" vue-slots="{{['default']}}"><slot name="title"></slot></ax-custom-title></block><block wx:else><ax-custom-title bind:display="__e" vue-id="2301dcd0-2" data-event-opts="{{[['^display',[['$emit',['display','$event']]]]]}}" bind:__l="__l"></ax-custom-title></block><view class="__root"><slot></slot></view><block wx:if="{{hideIndicatorArea==false}}"><ax-ios-indicator vue-id="2301dcd0-3" bind:__l="__l"></ax-ios-indicator></block></view>

+ 1 - 0
unpackage/dist/build/mp-weixin/components/ax-body/ax-body.wxss

@@ -0,0 +1 @@
+.ax-body{display:flex;flex-direction:column;height:100%}.ax-body .__root{flex:1;overflow:auto;padding-left:var(--blank)!important;padding-right:var(--blank)!important}ax-custom-title{position:relative;z-index:99999999}

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.js


+ 4 - 0
unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.json

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

+ 1 - 0
unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.wxml

@@ -0,0 +1 @@
+<block wx:if="{{visible}}"><view class="ax ax-custom-title data-v-bb38a6d6" style="{{'padding:'+(padding)+';'}}"><view class="__body data-v-bb38a6d6" style="{{'height:'+(height)+';'}}"><block wx:if="{{$slots.default}}"><slot></slot></block><block wx:else><view data-event-opts="{{[['tap',[['back']]]]}}" bindtap="__e" class="data-v-bb38a6d6"><block wx:if="{{$root.g0>1}}"><text class="icon-back data-v-bb38a6d6"></text></block><text class="title data-v-bb38a6d6">{{title||titleText}}</text></view></block></view></view></block>

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/components/ax-custom-title/ax-custom-title.wxss


+ 10 - 0
unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.js

@@ -0,0 +1,10 @@
+(global["webpackJsonp"]=global["webpackJsonp"]||[]).push([["components/ax-ios-indicator/ax-ios-indicator"],{"0158":function(t,e,n){"use strict";n.r(e);var i=n("c07f"),a=n.n(i);for(var r in i)["default"].indexOf(r)<0&&function(t){n.d(e,t,(function(){return i[t]}))}(r);e["default"]=a.a},2348:function(t,e,n){"use strict";n.r(e);var i=n("c729"),a=n("0158");for(var r in a)["default"].indexOf(r)<0&&function(t){n.d(e,t,(function(){return a[t]}))}(r);n("6a74");var u=n("828b"),c=Object(u["a"])(a["default"],i["b"],i["c"],!1,null,"aa69d142",null,!1,i["a"],void 0);e["default"]=c.exports},"354e":function(t,e,n){},"6a74":function(t,e,n){"use strict";var i=n("354e"),a=n.n(i);a.a},c07f:function(t,e,n){"use strict";(function(t){Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var n={name:"ax-ios-indicator",props:{offset:{type:[Number,String],default:10},min:{type:[Number,String],default:0}},computed:{height:function(){var e=t.getSystemInfoSync(),n=e.screenHeight-e.safeArea.bottom-(Number(this.offset)||0);return Math.max(Number(this.min)||0,n)},style:function(){return{height:"".concat(this.height,"px")}}}};e.default=n}).call(this,n("df3c")["default"])},c729:function(t,e,n){"use strict";n.d(e,"b",(function(){return i})),n.d(e,"c",(function(){return a})),n.d(e,"a",(function(){}));var i=function(){var t=this.$createElement,e=(this._self._c,this.__get_style([this.style]));this.$mp.data=Object.assign({},{$root:{s0:e}})},a=[]}}]);
+;(global["webpackJsonp"] = global["webpackJsonp"] || []).push([
+    'components/ax-ios-indicator/ax-ios-indicator-create-component',
+    {
+        'components/ax-ios-indicator/ax-ios-indicator-create-component':(function(module, exports, __webpack_require__){
+            __webpack_require__('df3c')['createComponent'](__webpack_require__("2348"))
+        })
+    },
+    [['components/ax-ios-indicator/ax-ios-indicator-create-component']]
+]);

+ 4 - 0
unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.json

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

+ 1 - 0
unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.wxml

@@ -0,0 +1 @@
+<view class="ax ax-ios-indicator data-v-aa69d142" style="{{$root.s0}}"></view>

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/components/ax-ios-indicator/ax-ios-indicator.wxss


+ 10 - 0
unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.js

@@ -0,0 +1,10 @@
+(global["webpackJsonp"]=global["webpackJsonp"]||[]).push([["components/ax-popup/ax-popup"],{"09cf":function(t,e,i){},2734:function(t,e,i){"use strict";i.r(e);var n=i("432e"),s=i.n(n);for(var o in n)["default"].indexOf(o)<0&&function(t){i.d(e,t,(function(){return n[t]}))}(o);e["default"]=s.a},"432e":function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var n={name:"ax-popup",emits:["open","opened","close","closed","mask"],props:{position:{type:String,default:""},maskEnable:{type:Boolean,default:!1},maskType:{type:String,default:""},maskBlur:{type:[Number,String],default:0},maskClose:{type:Boolean,default:!1}},data:function(){return{visible:!1,closing:!1}},computed:{CssSheet:function(){return[this.position,this.maskType,this.closing?"close":""]},StyleSheet:function(){return{"--mask-blur":"blur(".concat(this.maskBlur,"px)")}}},methods:{animationend:function(t){1==this.visible&&0==this.closing?this.$emit("opened"):(this.visible=!1,this.closing=!1,this.$emit("closed"))},mask:function(){this.maskClose&&this.close(),this.$emit("mask")},close:function(){this.closing=!0,this.$emit("close")},open:function(){this.visible=!0,this.closing=!1,this.$emit("open")}}};e.default=n},4393:function(t,e,i){"use strict";i.r(e);var n=i("666b"),s=i("2734");for(var o in s)["default"].indexOf(o)<0&&function(t){i.d(e,t,(function(){return s[t]}))}(o);i("5989");var u=i("828b"),a=Object(u["a"])(s["default"],n["b"],n["c"],!1,null,"7c1b6b93",null,!1,n["a"],void 0);e["default"]=a.exports},5989:function(t,e,i){"use strict";var n=i("09cf"),s=i.n(n);s.a},"666b":function(t,e,i){"use strict";i.d(e,"b",(function(){return n})),i.d(e,"c",(function(){return s})),i.d(e,"a",(function(){}));var n=function(){var t=this.$createElement,e=(this._self._c,this.visible?this.__get_style([this.StyleSheet]):null);this.$mp.data=Object.assign({},{$root:{s0:e}})},s=[]}}]);
+;(global["webpackJsonp"] = global["webpackJsonp"] || []).push([
+    'components/ax-popup/ax-popup-create-component',
+    {
+        'components/ax-popup/ax-popup-create-component':(function(module, exports, __webpack_require__){
+            __webpack_require__('df3c')['createComponent'](__webpack_require__("4393"))
+        })
+    },
+    [['components/ax-popup/ax-popup-create-component']]
+]);

+ 4 - 0
unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.json

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

+ 1 - 0
unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.wxml

@@ -0,0 +1 @@
+<block wx:if="{{visible}}"><view class="{{['ax','ax-popup','data-v-7c1b6b93',CssSheet]}}" style="{{$root.s0}}"><view data-event-opts="{{[['animationend',[['animationend']]]]}}" class="__body data-v-7c1b6b93" bindanimationend="__e"><slot></slot></view><block wx:if="{{maskEnable||maskClose}}"><view data-event-opts="{{[['tap',[['mask']]]]}}" class="__mask data-v-7c1b6b93" bindtap="__e"></view></block></view></block>

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/build/mp-weixin/components/ax-popup/ax-popup.wxss


+ 10 - 0
unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.js

@@ -0,0 +1,10 @@
+(global["webpackJsonp"]=global["webpackJsonp"]||[]).push([["components/r-canvas/r-canvas"],{"2fe5":function(n,t,e){"use strict";var u=e("8817"),a=e.n(u);a.a},4528:function(n,t,e){"use strict";e.r(t);var u=e("72f0"),a=e("ee5b");for(var c in a)["default"].indexOf(c)<0&&function(n){e.d(t,n,(function(){return a[n]}))}(c);e("2fe5");var r=e("828b"),f=Object(r["a"])(a["default"],u["b"],u["c"],!1,null,null,null,!1,u["a"],void 0);t["default"]=f.exports},"72f0":function(n,t,e){"use strict";e.d(t,"b",(function(){return u})),e.d(t,"c",(function(){return a})),e.d(t,"a",(function(){}));var u=function(){var n=this.$createElement;this._self._c},a=[]},8817:function(n,t,e){},c496:function(n,t,e){"use strict";var u=e("47a9");Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=u(e("47cc")),c={mixins:[a.default]};t.default=c},ee5b:function(n,t,e){"use strict";e.r(t);var u=e("c496"),a=e.n(u);for(var c in u)["default"].indexOf(c)<0&&function(n){e.d(t,n,(function(){return u[n]}))}(c);t["default"]=a.a}}]);
+;(global["webpackJsonp"] = global["webpackJsonp"] || []).push([
+    'components/r-canvas/r-canvas-create-component',
+    {
+        'components/r-canvas/r-canvas-create-component':(function(module, exports, __webpack_require__){
+            __webpack_require__('df3c')['createComponent'](__webpack_require__("4528"))
+        })
+    },
+    [['components/r-canvas/r-canvas-create-component']]
+]);

+ 4 - 0
unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.json

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

+ 1 - 0
unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.wxml

@@ -0,0 +1 @@
+<view><view class="{{['r-canvas-component',(hidden)?'hidden':'']}}" style="{{'width:'+(canvas_width/scale+'px')+';'+('height:'+(canvas_height/scale+'px')+';')}}"><block wx:if="{{canvas_id}}"><canvas class="r-canvas" style="{{'width:'+(canvas_width+'px')+';'+('height:'+(canvas_height+'px')+';')+('transform:'+('scale('+r_canvas_scale+')')+';')}}" canvas-id="{{canvas_id}}" id="{{canvas_id}}"></canvas></block></view></view>

+ 1 - 0
unpackage/dist/build/mp-weixin/components/r-canvas/r-canvas.wxss

@@ -0,0 +1 @@
+.r-canvas{-webkit-transform-origin:0 0;transform-origin:0 0}.r-canvas-component{overflow:hidden}.r-canvas-component.hidden{position:fixed;top:-5000rpx}

Some files were not shown because too many files changed in this diff