瀏覽代碼

项目初始化

linwu 1 年之前
父節點
當前提交
e621b8d816
共有 100 個文件被更改,包括 25434 次插入3 次删除
  1. 1 0
      .gitignore
  2. 66 0
      App.vue
  3. 0 3
      README.md
  4. 194 0
      common/app.css
  5. 21 0
      common/config.js
  6. 194 0
      common/httpRequest.js
  7. 115 0
      common/pay.js
  8. 116 0
      common/request.js
  9. 375 0
      common/sam.js
  10. 93 0
      common/we7_js/base64.js
  11. 273 0
      common/we7_js/md5.js
  12. 1545 0
      common/we7_js/underscore.js
  13. 1022 0
      common/we7_js/util.js
  14. 232 0
      components/Pengpai-FadeInOut/Pengpai-FadeInOut.vue
  15. 7 0
      components/common/tui-clipboard/clipboard.min.js
  16. 53 0
      components/common/tui-clipboard/tui-clipboard.js
  17. 286 0
      components/common/tui-poster/tui-poster.js
  18. 268 0
      components/common/tui-validation/tui-validation.js
  19. 0 0
      components/common/tui-validation/tui-validation.min.js
  20. 814 0
      components/jyf-parser/jyf-parser.vue
  21. 102 0
      components/jyf-parser/libs/CssHandler.js
  22. 577 0
      components/jyf-parser/libs/MpHtmlParser.js
  23. 80 0
      components/jyf-parser/libs/config.js
  24. 35 0
      components/jyf-parser/libs/handler.sjs
  25. 44 0
      components/jyf-parser/libs/handler.wxs
  26. 476 0
      components/jyf-parser/libs/trees.vue
  27. 105 0
      components/pretty-times/pretty-times.scss
  28. 322 0
      components/pretty-times/pretty-times.vue
  29. 137 0
      components/privacyPopup/privacyPopup.vue
  30. 185 0
      components/thorui/tui-actionsheet/tui-actionsheet.vue
  31. 134 0
      components/thorui/tui-alert/tui-alert.vue
  32. 155 0
      components/thorui/tui-badge/tui-badge.vue
  33. 387 0
      components/thorui/tui-bottom-navigation/tui-bottom-navigation.vue
  34. 106 0
      components/thorui/tui-bottom-popup/tui-bottom-popup.vue
  35. 203 0
      components/thorui/tui-bubble-popup/tui-bubble-popup.vue
  36. 519 0
      components/thorui/tui-button/tui-button.vue
  37. 562 0
      components/thorui/tui-calendar/tui-calendar.js
  38. 864 0
      components/thorui/tui-calendar/tui-calendar.vue
  39. 211 0
      components/thorui/tui-card/tui-card.vue
  40. 521 0
      components/thorui/tui-cascade-selection/tui-cascade-selection.vue
  41. 257 0
      components/thorui/tui-circular-progress/tui-circular-progress.vue
  42. 166 0
      components/thorui/tui-collapse/tui-collapse.vue
  43. 335 0
      components/thorui/tui-countdown/tui-countdown.vue
  44. 548 0
      components/thorui/tui-datetime/tui-datetime.vue
  45. 103 0
      components/thorui/tui-divider/tui-divider.vue
  46. 139 0
      components/thorui/tui-drawer/tui-drawer.vue
  47. 69 0
      components/thorui/tui-dropdown-list/tui-dropdown-list.vue
  48. 261 0
      components/thorui/tui-fab/tui-fab.vue
  49. 118 0
      components/thorui/tui-footer/tui-footer.vue
  50. 147 0
      components/thorui/tui-grid-item/tui-grid-item.vue
  51. 44 0
      components/thorui/tui-grid/tui-grid.vue
  52. 55 0
      components/thorui/tui-icon/tui-icon.vue
  53. 1030 0
      components/thorui/tui-image-cropper/tui-image-cropper.vue
  54. 163 0
      components/thorui/tui-image-group/tui-image-group.vue
  55. 73 0
      components/thorui/tui-keyboard-input/tui-keyboard-input.vue
  56. 240 0
      components/thorui/tui-keyboard/tui-keyboard.vue
  57. 172 0
      components/thorui/tui-list-cell/tui-list-cell.vue
  58. 97 0
      components/thorui/tui-list-view/tui-list-view.vue
  59. 78 0
      components/thorui/tui-loading/tui-loading.vue
  60. 161 0
      components/thorui/tui-loadmore/tui-loadmore.vue
  61. 407 0
      components/thorui/tui-modal/tui-modal.vue
  62. 250 0
      components/thorui/tui-navigation-bar/tui-navigation-bar.vue
  63. 117 0
      components/thorui/tui-no-data/tui-no-data.vue
  64. 115 0
      components/thorui/tui-nomore/tui-nomore.vue
  65. 205 0
      components/thorui/tui-numberbox/tui-numberbox.vue
  66. 699 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.vue
  67. 560 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.wxs
  68. 161 0
      components/thorui/tui-rate/tui-rate.vue
  69. 307 0
      components/thorui/tui-round-progress/tui-round-progress.vue
  70. 178 0
      components/thorui/tui-scroll-top/tui-scroll-top.vue
  71. 239 0
      components/thorui/tui-skeleton/tui-skeleton.vue
  72. 217 0
      components/thorui/tui-slide-verify/tui-slide-verify.vue
  73. 73 0
      components/thorui/tui-slide-verify/tui-slide-verify.wxs
  74. 254 0
      components/thorui/tui-steps/tui-steps.vue
  75. 124 0
      components/thorui/tui-sticky-wxs/tui-sticky-wxs.vue
  76. 44 0
      components/thorui/tui-sticky-wxs/tui-sticky.wxs
  77. 152 0
      components/thorui/tui-sticky/tui-sticky.vue
  78. 310 0
      components/thorui/tui-swipe-action/tui-swipe-action.vue
  79. 273 0
      components/thorui/tui-tab/tui-tab.vue
  80. 306 0
      components/thorui/tui-tabbar/tui-tabbar.vue
  81. 303 0
      components/thorui/tui-tabs/tui-tabs.vue
  82. 354 0
      components/thorui/tui-tag/tui-tag.vue
  83. 38 0
      components/thorui/tui-time-axis/tui-time-axis.vue
  84. 50 0
      components/thorui/tui-timeaxis-item/tui-timeaxis-item.vue
  85. 329 0
      components/thorui/tui-timer/tui-timer.vue
  86. 129 0
      components/thorui/tui-tips/tui-tips.vue
  87. 121 0
      components/thorui/tui-toast/tui-toast.vue
  88. 104 0
      components/thorui/tui-top-dropdown/tui-top-dropdown.vue
  89. 373 0
      components/thorui/tui-upload/tui-upload.vue
  90. 400 0
      components/uni-notice-bar/uni-notice-bar.vue
  91. 79 0
      components/utils/date.js
  92. 130 0
      components/views/app-plus/tui-share/tui-share.nvue
  93. 477 0
      components/views/diyfields/diyfields.vue
  94. 271 0
      components/views/diyfields/diyfieldsview.vue
  95. 30 0
      components/views/diyitem/blank.vue
  96. 73 0
      components/views/diyitem/coupon.vue
  97. 1264 0
      components/views/diyitem/diyapge.css
  98. 39 0
      components/views/diyitem/diyvideo.vue
  99. 70 0
      components/views/diyitem/duo.vue
  100. 153 0
      components/views/diyitem/goods.vue

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/unpackage

+ 66 - 0
App.vue

@@ -0,0 +1,66 @@
+<script>
+	export default {
+		globalData: {
+			privacyContractName: '', //需要弹窗展示的隐私协议名称
+			showPrivacy: false, //全局控制弹窗显隐
+			memberInfo: null,
+			uid: null
+		},
+		onLaunch: function() {
+			let that = this;
+			// #ifdef MP-WEIXIN
+			wx.getPrivacySetting({
+				success(res) {
+					console.log('是否需要授权:', res.needAuthorization, '隐私协议的名称为:', res.privacyContractName);
+					if (res.needAuthorization) {
+						that.globalData.privacyContractName = res.privacyContractName;
+						that.globalData.showPrivacy = true;
+					} else {
+						that.globalData.showPrivacy = false;
+					}
+				}
+			});
+			if (wx.canIUse('getUpdateManager')) {
+				const updateManager = wx.getUpdateManager();
+				updateManager.onCheckForUpdate(function(res) {
+					// 请求完新版本信息的回调
+					if (res.hasUpdate) {
+						updateManager.onUpdateReady(function() {
+							that.tui.modal('更新提示', '新版本已经上线啦~,为了获得更好的体验,建议立即更新', false, res => {
+								// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
+								updateManager.applyUpdate();
+							});
+						});
+						updateManager.onUpdateFailed(function() {
+							// 新的版本下载失败
+							that.tui.modal('更新失败', '新版本更新失败,为了获得更好的体验,请您删除当前小程序,重新搜索打开', false,
+								res => {});
+						});
+					}
+				});
+			}
+			// #endif
+		},
+		onShow: function() {
+			//console.log('sam ok--');
+		},
+		onHide: function() {
+			//console.log('App Hide')
+		},
+		onError: function(err) {
+			//全局错误监听
+			// #ifdef APP-PLUS
+			plus.runtime.getProperty(plus.runtime.appid, widgetInfo => {
+				const res = uni.getSystemInfoSync();
+				let errMsg =
+					`手机品牌:${res.brand};手机型号:${res.model};操作系统版本:${res.system};客户端平台:${res.platform};错误描述:${err}`;
+				console.log('发生错误:' + errMsg);
+			});
+			// #endif
+		}
+	};
+</script>
+
+<style>
+	@import './common/app.css';
+</style>

+ 0 - 3
README.md

@@ -1,3 +0,0 @@
-# jbangjia_applet
-
-晋帮家小程序

+ 194 - 0
common/app.css

@@ -0,0 +1,194 @@
+/*app.wxss*/
+/* #ifndef APP-NVUE */
+page {
+	background-color: #fafafa;
+	font-size: 32rpx;
+}
+
+::-webkit-scrollbar {
+	width: 0 !important;
+	height: 0 !important;
+	color: transparent !important;
+	display: none;
+}
+
+button::after {
+	border: none;
+}
+
+.container {
+	display: flex;
+	box-sizing: border-box;
+	flex-direction: column;
+}
+
+.tui-phcolor {
+	color: #ccc;
+	font-size: 32rpx;
+	overflow: visible;
+}
+
+
+
+.tui-opcity {
+	opacity: 0.5;
+}
+
+.tui-hover {
+	background-color: #f7f7f9 !important;
+}
+
+.tui-ellipsis {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+
+
+/*按钮样式*/
+
+/*列表统一样式 */
+.tui-list-item {
+	position: relative;
+}
+
+.tui-list-item::after {
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+	bottom: 0;
+	right: 0;
+	left: 30rpx;
+}
+
+.tui-last::after {
+	border-bottom: 0 !important;
+}
+
+.tui-button-primary {
+	width: 100%;
+	height: 90rpx;
+	line-height: 90rpx;
+	background: linear-gradient(-90deg, #5677fc, #5c8dff);
+	border-radius: 45rpx;
+	color: #fff;
+	font-size: 36rpx;
+}
+
+.tui-button-hover {
+	color: #d5d4d9;
+	background: linear-gradient(-90deg, #4a67d6, #4e77d9);
+}
+
+.tui-button-gray {
+	background: #ededed;
+	color: #999 !important;
+}
+
+.tui-button-gray_hover {
+	background: #d5d5d5 !important;
+	color: #898989;
+}
+
+.tui-button-white {
+	background: #fff;
+	color: #333 !important;
+}
+
+.tui-button-white_hover {
+	background: #e5e5e5 !important;
+	color: #2e2e2e !important;
+}
+.f-34 {
+  font-size: 34rpx;
+}
+
+.f-32 {
+  font-size: 32rpx;
+}
+
+.f-31 {
+  font-size: 31rpx;
+}
+
+.f-30 {
+  font-size: 30rpx;
+}
+
+.f-29 {
+  font-size: 29rpx;
+}
+
+.f-28 {
+  font-size: 28rpx;
+}
+
+.f-26 {
+  font-size: 26rpx;
+}
+
+.f-24 {
+  font-size: 24rpx;
+}
+
+.f-22 {
+  font-size: 22rpx;
+}
+.dis-flex {
+  display: flex;
+  /* flex-wrap: wrap; */
+}
+
+.flex-box {
+  flex: 1;
+}
+
+.flex-dir-row {
+  flex-direction: row;
+}
+
+.flex-dir-column {
+  flex-direction: column;
+}
+
+.flex-x-center {
+  /* display: flex; */
+  justify-content: center;
+}
+
+.flex-x-between {
+  justify-content: space-between;
+}
+
+.flex-x-around {
+  justify-content: space-around;
+}
+
+.flex-x-end {
+  justify-content: flex-end;
+}
+
+.flex-y-center {
+  /* display: flex; */
+  align-items: center;
+}
+
+.flex-five {
+  box-sizing: border-box;
+  flex: 0 0 50%;
+}
+
+.flex-three {
+  float: left;
+  width: 33.3%;
+}
+
+.flex-four {
+  box-sizing: border-box;
+  flex: 0 0 25%;
+}
+
+/* #endif */

+ 21 - 0
common/config.js

@@ -0,0 +1,21 @@
+import util from '@/common/we7_js/util.js'
+module.exports = {
+  getConf: function (t) {
+    var e = uni.getStorageSync("appConfig");
+    for (var o in e)
+    {
+     if (o == t) 
+     { 
+       return e[o];
+     }
+     }
+  },
+  init: function (e) {
+    util.request({
+      url: "entry/wxapp/config",
+      success: function (t) {
+        uni.setStorageSync("appConfig", t.data.data), "function" == typeof e && e();
+      }
+    });
+  },
+};

+ 194 - 0
common/httpRequest.js

@@ -0,0 +1,194 @@
+/**
+ * 常用方法封装 请求,文件上传等
+ * @author echo. 
+ **/
+
+const tui = {
+	//接口地址
+	interfaceUrl: function() {
+		return 'https://www.abc.com'
+	},
+	toast: function(text, duration, success) {
+		uni.showToast({
+			title: text || "出错啦~",
+			icon: success ? 'success' : 'none',
+			duration: duration || 2000
+		})
+	},
+	modal: function(title, content, showCancel, callback, confirmColor, confirmText) {
+		uni.showModal({
+			title: title || '提示',
+			content: content,
+			showCancel: showCancel,
+			cancelColor: "#555",
+			confirmColor: confirmColor || "#5677fc",
+			confirmText: confirmText || "确定",
+			success(res) {
+				if (res.confirm) {
+					callback && callback(true)
+				} else {
+					callback && callback(false)
+				}
+			}
+		})
+	},
+	isAndroid: function() {
+		const res = uni.getSystemInfoSync();
+		return res.platform.toLocaleLowerCase() == "android"
+	},
+	isPhoneX: function() {
+		const res = uni.getSystemInfoSync();
+		let iphonex = false;
+		let models = ['iphonex', 'iphonexr', 'iphonexsmax', 'iphone11', 'iphone11pro', 'iphone11promax']
+		const model = res.model.replace(/\s/g, "").toLowerCase()
+		if (models.includes(model)) {
+			iphonex = true;
+		}
+		return iphonex;
+	},
+	constNum: function() {
+		let time = 0;
+		// #ifdef APP-PLUS
+		time = this.isAndroid() ? 300 : 0;
+		// #endif
+		return time
+	},
+	delayed: null,
+	showLoading: function(title, mask = true) {
+		uni.showLoading({
+			mask: mask,
+			title: title || '请稍候...'
+		})
+	},
+	/**
+	 * 请求数据处理
+	 * @param string url 请求地址
+	 * @param string method 请求方式
+	 *  GET or POST
+	 * @param {*} postData 请求参数
+	 * @param bool isDelay 是否延迟显示loading
+	 * @param bool isForm 数据格式
+	 *  true: 'application/x-www-form-urlencoded'
+	 *  false:'application/json'
+	 * @param bool hideLoading 是否隐藏loading
+	 *  true: 隐藏
+	 *  false:显示
+	 */
+	request: async function(url, method, postData, isDelay, isForm, hideLoading) {
+		//接口请求
+		let loadding = false;
+		tui.delayed && uni.hideLoading();
+		clearTimeout(tui.delayed);
+		tui.delayed = null;
+		if (!hideLoading) {
+			if (isDelay) {
+				tui.delayed = setTimeout(() => {
+					loadding = true
+					tui.showLoading()
+				}, 1000)
+			} else {
+				loadding = true
+				tui.showLoading()
+			}
+		}
+
+		return new Promise((resolve, reject) => {
+			uni.request({
+				url: tui.interfaceUrl() + url,
+				data: postData,
+				header: {
+					'content-type': isForm ? 'application/x-www-form-urlencoded' : 'application/json',
+					'Authorization': tui.getToken()
+				},
+				method: method, //'GET','POST'
+				dataType: 'json',
+				success: (res) => {
+					clearTimeout(tui.delayed)
+					tui.delayed = null;
+					if (loadding && !hideLoading) {
+						uni.hideLoading()
+					}
+					resolve(res.data)
+				},
+				fail: (res) => {
+					clearTimeout(tui.delayed)
+					tui.delayed = null;
+					tui.toast("网络不给力,请稍后再试~")
+					reject(res)
+				}
+			})
+		})
+	},
+	/**
+	 * 上传文件
+	 * @param string url 请求地址
+	 * @param string src 文件路径
+	 */
+	uploadFile: function(url, src) {
+		tui.showLoading()
+		return new Promise((resolve, reject) => {
+			const uploadTask = uni.uploadFile({
+				url: tui.interfaceUrl() + url,
+				filePath: src,
+				name: 'imageFile',
+				header: {
+					'Authorization': tui.getToken()
+				},
+				formData: {
+					// sizeArrayText:""
+				},
+				success: function(res) {
+					uni.hideLoading()
+					let d = JSON.parse(res.data.replace(/\ufeff/g, "") || "{}")
+					if (d.code % 100 == 0) {
+						//返回图片地址
+						let fileObj = d.data;
+						resolve(fileObj)
+					} else {
+						that.toast(res.msg);
+					}
+				},
+				fail: function(res) {
+					reject(res)
+					that.toast(res.msg);
+				}
+			})
+		})
+	},
+	tuiJsonp: function(url, callback, callbackname) {
+		// #ifdef H5
+		window[callbackname] = callback;
+		let tuiScript = document.createElement("script");
+		tuiScript.src = url;
+		tuiScript.type = "text/javascript";
+		document.head.appendChild(tuiScript);
+		document.head.removeChild(tuiScript);
+		// #endif
+	},
+	//设置用户信息
+	setUserInfo: function(mobile, token) {
+		uni.setStorageSync("thorui_mobile", mobile)
+	},
+	//获取token
+	getToken() {
+		return uni.getStorageSync("thorui_token")
+	},
+	//判断是否登录
+	isLogin: function() {
+		return uni.getStorageSync("thorui_mobile") ? true : false
+	},
+	//跳转页面,校验登录状态
+	href(url, isVerify) {
+		if (isVerify && !tui.isLogin()) {
+			uni.navigateTo({
+				url: '/pages/common/login/login'
+			})
+		} else {
+			uni.navigateTo({
+				url: url
+			});
+		}
+	}
+}
+
+export default tui

+ 115 - 0
common/pay.js

@@ -0,0 +1,115 @@
+import request from '@/common/request.js'
+/**
+ * type: order 支付订单 recharge paybill 优惠买单
+ * data: 扩展数据对象,用于保存参数
+ */
+module.exports = {
+	wxpay: function(type, money, orderid, redirectUrl) {
+		request.post('pay', {
+			orderid: orderid,
+			money: money,
+			type: type
+		}).then(res => {
+			uni.showLoading({
+				title: '正在支付中...'
+			})
+
+			if (res.errno == 0) {
+				// #ifdef MP-WEIXIN
+				// 发起支付
+				uni.requestPayment({
+					'timeStamp': res.data.timeStamp,
+					'nonceStr': res.data.nonceStr,
+					'package': res.data.package,
+					'signType': 'MD5',
+					'paySign': res.data.paySign,
+					fail: function(err) {
+						console.log(err.errMsg);
+						if (err.errMsg == 'requestPayment:fail cancel'){
+							uni.hideLoading();
+							uni.showModal({
+								title: '提示',
+								content: '取消支付',
+								showCancel: false,
+								success: function(res) {
+								}
+							})
+						}else{
+							uni.hideLoading();
+							uni.showModal({
+								title: '支付失败',
+								content: JSON.stringify(err),
+								showCancel: false,
+								success: function(res) {
+								}
+							})
+						}
+						
+						
+					},
+					success: function() {
+						uni.hideLoading();
+						// 提示支付成功
+						uni.showToast({
+							title: "支付成功",
+							duration: 2000,
+							icon: 'success_no_circle',
+							size: "10"
+						})
+						uni.redirectTo({
+							url: redirectUrl
+						});
+					}
+				})
+				// #endif
+				
+				// #ifdef H5
+				
+				WeixinJSBridge.invoke('getBrandWCPayRequest', {
+					"appId": res.data.appId, //公众号名称,由商户传入
+					"timeStamp": res.data.timeStamp, //时间戳
+					"nonceStr": res.data.nonceStr, //随机串
+					"package": res.data.package, //扩展包
+					"signType": 'MD5', //微信签名方式:MD5
+					"paySign": res.data.paySign //微信签名
+				}, function(respay) {
+					if (respay.err_msg === "get_brand_wcpay_request:ok") {
+						uni.redirectTo({
+							url: redirectUrl
+						});
+						//that.getData();
+					} else if (respay.err_msg === "get_brand_wcpay_request:cancel") {
+						uni.showToast({
+							title: "取消支付",
+							icon: "none",
+							duration: 2000
+						});
+					} else if (respay.err_msg === "get_brand_wcpay_request:fail") {
+						uni.showToast({
+							title: "支付失败",
+							icon: "none",
+							duration: 2000
+						})
+					}
+				}, function(err) {
+					uni.showToast({
+						title: res.data.msg,
+						icon: "none",
+						duration: 2000
+					});
+				});
+				// #endif
+			} else {
+				console.log('支付出错')
+				uni.showModal({
+					title: '出错了',
+					content: res.code + ':' + res.msg + ':' + res.data,
+					showCancel: false,
+					success: function(res) {
+
+					}
+				})
+			}
+		})
+	}
+}

+ 116 - 0
common/request.js

@@ -0,0 +1,116 @@
+import util from '@/common/we7_js/util.js'
+import App from '../App'
+module.exports = {
+	// get请求后台数据
+	get: function(url, data) {
+		return new Promise((resolve, reject) => {
+			util.request({
+				'url': 'entry/wxapp/' + url,
+				'showLoading': data ? data.showLoading : false,
+				'data': data,
+				'method': 'get',
+				'cachetime': '30',
+				success: function(res) {
+					resolve(res.data)
+				}
+			});
+		})
+	},
+	// post请求后台数据
+	post: function(url, data) {
+		return new Promise((resolve, reject) => {
+			util.request({
+				'url': 'entry/wxapp/' + url,
+				'data': data,
+				'showLoading': data ? data.showLoading : false,
+				'method': 'post',
+				'cachetime': '30',
+				success: function(res) {
+					resolve(res.data)
+				}
+			});
+		})
+	},
+	uploadFile: function(path) {
+		var formvar = 'wxapp';
+		var url = '';
+
+		//#ifdef H5
+		formvar = 'mp';
+		//#endif
+		url = App.siteInfo.siteroot + '?i=' + App.siteInfo.uniacid +
+			'&c=utility&a=file&do=upload&thumb=0&from=' + formvar;
+		//#ifdef H5
+		let urlquery = getQuery(window.location.href);
+		if (urlquery.length > 0) {
+			var urli = '';
+			for (let i = 0; i < urlquery.length; i++) {
+				if (urlquery[i] && urlquery[i].name && urlquery[i].value) {
+					if (urlquery[i].name == "i") {
+						urli = urlquery[i].name + '=' + urlquery[i].value;
+					}
+				}
+			}
+			if (urli) {
+				url = window.location.protocol + '//' + window.location.host +
+					'/app/index.php?c=utility&a=file&do=upload&thumb=0&from=' + formvar + '&' + urli;
+			}
+		}
+		//#endif
+
+		url = url + '&m=' + App.module;
+
+		return new Promise((resolve, reject) => {
+			var FilePaths = path;
+			console.log(FilePaths);
+			uni.uploadFile({
+				url: url,
+				method: 'POST',
+				// #ifdef MP-WEIXIN
+				filePath: FilePaths,
+				formData: {
+					file: FilePaths
+				},
+				header: {
+					"Content-Type": "multipart/form-data"
+				},
+				// #endif
+
+				// #ifdef H5
+
+				filePath: FilePaths,
+				formData: {
+					file: FilePaths
+				},
+				// #endif
+				name: 'file',
+				success: (res) => {
+					resolve(JSON.parse(res.data));
+					//resolve(res)
+					console.info("服务器返回的图片路径是:" + res.data)
+				}
+			});
+		});
+	}
+
+}
+
+function getQuery(url) {
+	var theRequest = [];
+	if (url.indexOf("?") != -1) {
+		var str = url.split('?')[1];
+		if (str.indexOf("#") != -1) {
+			str = str.split('#')[0]
+		}
+		var strs = str.split("&");
+		for (var i = 0; i < strs.length; i++) {
+			if (strs[i].split("=")[0] && unescape(strs[i].split("=")[1])) {
+				theRequest[i] = {
+					'name': strs[i].split("=")[0],
+					'value': unescape(strs[i].split("=")[1])
+				}
+			}
+		}
+	}
+	return theRequest;
+}

+ 375 - 0
common/sam.js

@@ -0,0 +1,375 @@
+import util from '@/common/we7_js/util.js'
+import request from '@/common/request.js'
+module.exports = {
+		/**
+		 * 跳转到指定页面
+		 * 支持tabBar页面
+		 */
+		navigateTo: function(url) {
+			console.log(getCurrentPages().length);
+			if (getCurrentPages().length < 6) {
+				uni.navigateTo({
+					url: url
+				});
+			} else {
+				uni.reLaunch({
+					url: url
+				});
+			}
+		},
+		diynavigateTo: function(e) {
+			var link = e.currentTarget.dataset.url;
+			if (link.ptype == 'custom') {
+				if (link.zdyLinktype == 'wxapp') {
+					uni.navigateToMiniProgram({
+						appId: link.zdyappid,
+						path: link.path
+					})
+				} else if (link.zdyLinktype == 'web') {
+					this.navigateTo("/pages/webview/h5?url=" + link.path)
+				} else {
+					this.navigateTo(link.path)
+				}
+
+			} else {
+				this.navigateTo(link.path)
+			}
+
+		},
+
+		geturli: function() {
+			let url = window.location.href;
+			let urli = {
+				i: 0
+			};
+			if (url.indexOf("?") != -1) {
+				var str = url.split('?')[1];
+				var strs = str.split("&");
+				for (var i = 0; i < strs.length; i++) {
+					if (strs[i].split("=")[0] && unescape(strs[i].split("=")[1])) {
+						if (strs[i].split("=")[0] == "i") {
+							urli.i = unescape(strs[i].split("=")[1]);
+						}
+					}
+				}
+			}
+			return urli.i;
+		},
+		//设置缓存 (单位为秒)
+		setStorage: function(key = ACCESS_TOKEN, value) {
+			const params = {
+				date: new Date().getTime(),
+				value
+			};
+			uni.setStorageSync(key, JSON.stringify(params));
+		},
+		getStorage: function(key = ACCESS_TOKEN, day = 0.5) {
+			let obj = uni.getStorageSync(key);
+			if (!obj) return null;
+			obj = JSON.parse(obj);
+			const date = new Date().getTime();
+			if (date - obj.date > 86400000 * day) return null;
+			return obj.value;
+		},
+		/**
+		 * 判断变量是否为空,
+		 * @param  {[type]}  param 变量
+		 * @return {Boolean}      为空返回true,否则返回false。
+		 */
+		isEmpty: function(param) {
+			if (param) {
+				var param_type = typeof(param);
+				if (param_type == 'object') {
+					//要判断的是【对象】或【数组】或【null】等
+					if (typeof(param.length) == 'undefined') {
+						if (JSON.stringify(param) == "{}") {
+							return true; //空值,空对象
+						}
+					} else if (param.length == 0) {
+						return true; //空值,空数组
+					}
+				} else if (param_type == 'string') {
+					//如果要过滤空格等字符
+					var new_param = param.trim();
+					if (new_param.length == 0) {
+						//空值,例如:带有空格的字符串" "。
+						return true;
+					}
+				} else if (param_type == 'boolean') {
+					if (!param) {
+						return true;
+					}
+				} else if (param_type == 'number') {
+					if (!param) {
+						return true;
+					}
+				}
+				return false; //非空值
+			} else {
+				//空值,例如:
+				//(1)null
+				//(2)可能使用了js的内置的名称,例如:var name=[],这个打印类型是字符串类型。
+				//(3)空字符串''、""。
+				//(4)数字0、00等,如果可以只输入0,则需要另外判断。
+				return true;
+			}
+		},
+		setUserGlobalData: function(param) {
+			if (param) {
+				getApp().globalData.memberInfo = param;
+				getApp().globalData.uid = param.uid;
+			}
+		},
+		onShowlogin: function() {
+			if (uni.getStorageSync('memberInfo')) {
+				this.login();
+			}
+		},
+		checktelephone: function() {
+			return new Promise((resolve, reject) => {
+				var _this = this;
+				util.getUserInfo(function(userInfo) {
+					request.post('member.checktelephone', {
+						samkey: (new Date()).valueOf()
+					}).then(function(res) {
+						if (res.data.is_gettelephone == 0) {
+							uni.showToast({
+								title: '您还未登录!',
+								icon: 'success',
+								duration: 1500
+							});
+							uni.reLaunch({
+								url: "/pages/login/login?ptype=member",
+							})
+						} else {
+							resolve(res.data);
+						}
+					})
+				})
+				// #ifdef APP-PLUS
+				resolve({
+					"uid": ''
+				})
+				// #endif
+			})
+		},
+		login: function() {
+			return new Promise((resolve, reject) => {
+				var _this = this;
+				var memberInfo = _this.getStorage("memberInfo", 0.001);
+				if (memberInfo) {
+					_this.setUserGlobalData(memberInfo);
+					//console.log('m1');
+					resolve(memberInfo)
+				} else {
+					//console.log('m2');
+					util.getUserInfo(function(userInfo) {
+						request.post('member.login', {
+							samkey: (new Date()).valueOf()
+						}).then(function(res) {
+							if (res.data.errno == 0) {
+								//console.log(res.data);
+								_this.setUserGlobalData(res.data);
+								_this.setStorage("memberInfo", res.data)
+								resolve(res.data)
+							} else if (res.data.errno == 20001) {
+								uni.showToast({
+									title: '账号审核中!',
+									icon: 'success',
+									duration: 1500
+								});
+								uni.redirectTo({
+									url: "/pages/login/success",
+								})
+							} else if (res.data.errno == 10001) {
+								uni.showToast({
+									title: '您还未登录!',
+									icon: 'success',
+									duration: 1500
+								});
+								uni.reLaunch({
+									url: "/pages/login/login?ptype=member",
+								})
+							}
+						})
+					})
+				}
+				// #ifdef APP-PLUS
+				resolve({
+					"uid": ''
+				})
+				// #endif
+			})
+		},
+
+		//获取定位信息
+		getCityPosition: function(param) {
+			return new Promise((resolve, reject) => {
+					var _this = this;
+					if (!param) {
+						param = {}
+					}
+					param.samkey = (new Date()).valueOf();
+					util.getUserInfo(function(userInfo) {
+							request.post('operatingcity.getcity', param).then(function(res) {
+									if (res.is_nulldate == 1) {
+									console.log('is_store');
+									console.log(res.is_nulldate);
+									resolve(res.data);
+								} else {
+									// #ifdef MP-WEIXIN
+									if (res.is_close_getposition != 1) {
+										wx.authorize({
+											scope: 'scope.userFuzzyLocation',
+											success: res => {
+												//console.log(res)
+												wx.getFuzzyLocation({
+													type: 'wgs84',
+													success(res) {
+														uni.setStorageSync(
+															'latitude', res
+															.latitude);
+														uni.setStorageSync(
+															'longitude', res
+															.longitude);
+														//console.log(res);	
+														request.post(
+															'operatingcity.getcity', {
+																samkey: (
+																		new Date()
+																	)
+																	.valueOf(),
+																latitude: res
+																	.latitude,
+																longitude: res
+																	.longitude
+															}).then(res => {
+
+															resolve(res
+																.data
+															);
+														});
+													}
+												});
+											},
+											fail: res => {
+												//console.log('失败:', res);
+												resolve(res);
+											}
+										});
+									}
+									// #endif
+
+									//#ifdef H5  || APP-PLUS
+									uni.getLocation({
+										type: 'wgs84',
+										success: res => {
+											//alert(res.latitude);
+											uni.setStorageSync('latitude', res
+												.latitude);
+											uni.setStorageSync('longitude', res
+												.longitude);
+											//console.log(res);	
+
+											request.post('operatingcity.getcity', {
+												samkey: (new Date()).valueOf(),
+												latitude: res.latitude,
+												longitude: res.longitude
+											}).then(res => {
+												resolve(res.data);
+											});
+										},
+										fail: res => {
+											//console.log('失败:', res);
+											resolve(res);
+										}
+									})
+									//#endif
+								}
+							})
+					})
+			})
+	},
+	/**
+	 * 保存图片
+	 */
+	saveImage(url) {
+		let that = this;
+		// 向用户发起授权请求
+		uni.authorize({
+			scope: 'scope.writePhotosAlbum',
+			success: () => {
+				// 已授权
+				that.downLoadImg(url);
+			},
+			fail: () => {
+				// 拒绝授权,获取当前设置
+				uni.getSetting({
+					success: (result) => {
+						if (!result.authSetting['scope.writePhotosAlbum']) {
+							that.isAuth()
+						}
+					}
+				});
+			}
+		})
+	},
+	/**
+	 * 下载资源,保存图片到系统相册
+	 */
+	downLoadImg(url) {
+		uni.showLoading({
+			title: '加载中'
+		});
+		uni.downloadFile({
+			url: url,
+			success: (res) => {
+				uni.hideLoading();
+				if (res.statusCode === 200) {
+					uni.saveImageToPhotosAlbum({
+						filePath: res.tempFilePath,
+						success: function() {
+							uni.showToast({
+								title: "保存成功",
+								icon: "none"
+							});
+						},
+						fail: function() {
+							uni.showToast({
+								title: "保存失败,请稍后重试",
+								icon: "none"
+							});
+						}
+					});
+				}
+			},
+			fail: (err) => {
+				uni.showToast({
+					title: "失败啦",
+					icon: "none"
+				});
+			}
+		})
+	},
+	/*
+	 * 引导用户开启权限
+	 */
+	isAuth() {
+		uni.showModal({
+			content: '由于您还没有允许保存图片到您相册里,无法进行保存,请点击确定允许授权',
+			success: (res) => {
+				if (res.confirm) {
+					uni.openSetting({
+						success: (result) => {
+							console.log(result.authSetting);
+						}
+					});
+				}
+			}
+		});
+	},
+
+
+
+
+}

+ 93 - 0
common/we7_js/base64.js

@@ -0,0 +1,93 @@
+function base64_encode(str) {
+	var c1, c2, c3;
+	var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+	var i = 0, len = str.length, string = '';
+
+	while (i < len) {
+		c1 = str.charCodeAt(i++) & 0xff;
+		if (i == len) {
+			string += base64EncodeChars.charAt(c1 >> 2);
+			string += base64EncodeChars.charAt((c1 & 0x3) << 4);
+			string += "==";
+			break;
+		}
+		c2 = str.charCodeAt(i++);
+		if (i == len) {
+			string += base64EncodeChars.charAt(c1 >> 2);
+			string += base64EncodeChars.charAt(((c1 & 0x3) << 4)
+				| ((c2 & 0xF0) >> 4));
+			string += base64EncodeChars.charAt((c2 & 0xF) << 2);
+			string += "=";
+			break;
+		}
+		c3 = str.charCodeAt(i++);
+		string += base64EncodeChars.charAt(c1 >> 2);
+		string += base64EncodeChars.charAt(((c1 & 0x3) << 4)
+			| ((c2 & 0xF0) >> 4));
+		string += base64EncodeChars.charAt(((c2 & 0xF) << 2)
+			| ((c3 & 0xC0) >> 6));
+		string += base64EncodeChars.charAt(c3 & 0x3F)
+	}
+	return string
+}
+
+function base64_decode(str) {
+	var c1, c2, c3, c4;
+	var base64DecodeChars = new Array(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62,
+		-1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1,
+		-1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+		15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1,
+		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
+		43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);
+	var i = 0, len = str.length, string = '';
+
+	while (i < len) {
+		do {
+			c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
+		} while (i < len && c1 == -1);
+
+		if (c1 == -1)
+			break;
+
+		do {
+			c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
+		} while (i < len && c2 == -1);
+
+		if (c2 == -1)
+			break;
+
+		string += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
+
+		do {
+			c3 = str.charCodeAt(i++) & 0xff;
+			if (c3 == 61)
+				return string;
+
+			c3 = base64DecodeChars[c3]
+		} while (i < len && c3 == -1);
+
+		if (c3 == -1)
+			break;
+
+		string += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
+
+		do {
+			c4 = str.charCodeAt(i++) & 0xff;
+			if (c4 == 61)
+				return string;
+			c4 = base64DecodeChars[c4]
+		} while (i < len && c4 == -1);
+
+		if (c4 == -1)
+			break;
+
+		string += String.fromCharCode(((c3 & 0x03) << 6) | c4)
+	}
+	return string;
+}
+module.exports = {
+	'base64_encode': base64_encode,
+	'base64_decode': base64_decode
+};

+ 273 - 0
common/we7_js/md5.js

@@ -0,0 +1,273 @@
+/*
+ * JavaScript MD5
+ * https://github.com/blueimp/JavaScript-MD5
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ *
+ * Based on
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*global unescape, define, module */
+
+; (function ($) {
+	'use strict'
+
+	/*
+	* Add integers, wrapping at 2^32. This uses 16-bit operations internally
+	* to work around bugs in some JS interpreters.
+	*/
+	function safe_add(x, y) {
+		var lsw = (x & 0xFFFF) + (y & 0xFFFF)
+		var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
+		return (msw << 16) | (lsw & 0xFFFF)
+	}
+
+	/*
+	* Bitwise rotate a 32-bit number to the left.
+	*/
+	function bit_rol(num, cnt) {
+		return (num << cnt) | (num >>> (32 - cnt))
+	}
+
+	/*
+	* These functions implement the four basic operations the algorithm uses.
+	*/
+	function md5_cmn(q, a, b, x, s, t) {
+		return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
+	}
+	function md5_ff(a, b, c, d, x, s, t) {
+		return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t)
+	}
+	function md5_gg(a, b, c, d, x, s, t) {
+		return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t)
+	}
+	function md5_hh(a, b, c, d, x, s, t) {
+		return md5_cmn(b ^ c ^ d, a, b, x, s, t)
+	}
+	function md5_ii(a, b, c, d, x, s, t) {
+		return md5_cmn(c ^ (b | (~d)), a, b, x, s, t)
+	}
+
+	/*
+	* Calculate the MD5 of an array of little-endian words, and a bit length.
+	*/
+	function binl_md5(x, len) {
+		/* append padding */
+		x[len >> 5] |= 0x80 << (len % 32)
+		x[(((len + 64) >>> 9) << 4) + 14] = len
+
+		var i
+		var olda
+		var oldb
+		var oldc
+		var oldd
+		var a = 1732584193
+		var b = -271733879
+		var c = -1732584194
+		var d = 271733878
+
+		for (i = 0; i < x.length; i += 16) {
+			olda = a
+			oldb = b
+			oldc = c
+			oldd = d
+
+			a = md5_ff(a, b, c, d, x[i], 7, -680876936)
+			d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586)
+			c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819)
+			b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330)
+			a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897)
+			d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426)
+			c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341)
+			b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983)
+			a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416)
+			d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417)
+			c = md5_ff(c, d, a, b, x[i + 10], 17, -42063)
+			b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162)
+			a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682)
+			d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101)
+			c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290)
+			b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329)
+
+			a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510)
+			d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632)
+			c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713)
+			b = md5_gg(b, c, d, a, x[i], 20, -373897302)
+			a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691)
+			d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083)
+			c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335)
+			b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848)
+			a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438)
+			d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690)
+			c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961)
+			b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501)
+			a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467)
+			d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784)
+			c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473)
+			b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734)
+
+			a = md5_hh(a, b, c, d, x[i + 5], 4, -378558)
+			d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463)
+			c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562)
+			b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556)
+			a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060)
+			d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353)
+			c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632)
+			b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640)
+			a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174)
+			d = md5_hh(d, a, b, c, x[i], 11, -358537222)
+			c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979)
+			b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189)
+			a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487)
+			d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835)
+			c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520)
+			b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651)
+
+			a = md5_ii(a, b, c, d, x[i], 6, -198630844)
+			d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415)
+			c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905)
+			b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055)
+			a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571)
+			d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606)
+			c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523)
+			b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799)
+			a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359)
+			d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744)
+			c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380)
+			b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649)
+			a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070)
+			d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379)
+			c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259)
+			b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551)
+
+			a = safe_add(a, olda)
+			b = safe_add(b, oldb)
+			c = safe_add(c, oldc)
+			d = safe_add(d, oldd)
+		}
+		return [a, b, c, d]
+	}
+
+	/*
+	* Convert an array of little-endian words to a string
+	*/
+	function binl2rstr(input) {
+		var i
+		var output = ''
+		var length32 = input.length * 32
+		for (i = 0; i < length32; i += 8) {
+			output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF)
+		}
+		return output
+	}
+
+	/*
+	* Convert a raw string to an array of little-endian words
+	* Characters >255 have their high-byte silently ignored.
+	*/
+	function rstr2binl(input) {
+		var i
+		var output = []
+		output[(input.length >> 2) - 1] = undefined
+		for (i = 0; i < output.length; i += 1) {
+			output[i] = 0
+		}
+		var length8 = input.length * 8
+		for (i = 0; i < length8; i += 8) {
+			output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32)
+		}
+		return output
+	}
+
+	/*
+	* Calculate the MD5 of a raw string
+	*/
+	function rstr_md5(s) {
+		return binl2rstr(binl_md5(rstr2binl(s), s.length * 8))
+	}
+
+	/*
+	* Calculate the HMAC-MD5, of a key and some data (raw strings)
+	*/
+	function rstr_hmac_md5(key, data) {
+		var i
+		var bkey = rstr2binl(key)
+		var ipad = []
+		var opad = []
+		var hash
+		ipad[15] = opad[15] = undefined
+		if (bkey.length > 16) {
+			bkey = binl_md5(bkey, key.length * 8)
+		}
+		for (i = 0; i < 16; i += 1) {
+			ipad[i] = bkey[i] ^ 0x36363636
+			opad[i] = bkey[i] ^ 0x5C5C5C5C
+		}
+		hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8)
+		return binl2rstr(binl_md5(opad.concat(hash), 512 + 128))
+	}
+
+	/*
+	* Convert a raw string to a hex string
+	*/
+	function rstr2hex(input) {
+		var hex_tab = '0123456789abcdef'
+		var output = ''
+		var x
+		var i
+		for (i = 0; i < input.length; i += 1) {
+			x = input.charCodeAt(i)
+			output += hex_tab.charAt((x >>> 4) & 0x0F) +
+				hex_tab.charAt(x & 0x0F)
+		}
+		return output
+	}
+
+	/*
+	* Encode a string as utf-8
+	*/
+	function str2rstr_utf8(input) {
+		return unescape(encodeURIComponent(input))
+	}
+
+	/*
+	* Take string arguments and return either raw or hex encoded strings
+	*/
+	function raw_md5(s) {
+		return rstr_md5(str2rstr_utf8(s))
+	}
+	function hex_md5(s) {
+		return rstr2hex(raw_md5(s))
+	}
+	function raw_hmac_md5(k, d) {
+		return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))
+	}
+	function hex_hmac_md5(k, d) {
+		return rstr2hex(raw_hmac_md5(k, d))
+	}
+
+	function md5(string, key, raw) {
+		if (!key) {
+			if (!raw) {
+				return hex_md5(string)
+			}
+			return raw_md5(string)
+		}
+		if (!raw) {
+			return hex_hmac_md5(key, string)
+		}
+		return raw_hmac_md5(key, string)
+	}
+
+	module.exports = md5;
+}(this))

+ 1545 - 0
common/we7_js/underscore.js

@@ -0,0 +1,1545 @@
+//     Underscore.js 1.8.2
+//     http://underscorejs.org
+//     (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function () {
+
+	// Baseline setup
+	// --------------
+
+	// Establish the root object, `window` in the browser, or `exports` on the server.
+	//   var root = this;
+
+	//   // Save the previous value of the `_` variable.
+	//   var previousUnderscore = root._;
+
+	// Save bytes in the minified (but not gzipped) version:
+	var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+	// Create quick reference variables for speed access to core prototypes.
+	var
+		push = ArrayProto.push,
+		slice = ArrayProto.slice,
+		toString = ObjProto.toString,
+		hasOwnProperty = ObjProto.hasOwnProperty;
+
+	// All **ECMAScript 5** native function implementations that we hope to use
+	// are declared here.
+	var
+		nativeIsArray = Array.isArray,
+		nativeKeys = Object.keys,
+		nativeBind = FuncProto.bind,
+		nativeCreate = Object.create;
+
+	// Naked function reference for surrogate-prototype-swapping.
+	var Ctor = function () { };
+
+	// Create a safe reference to the Underscore object for use below.
+	var _ = function (obj) {
+		if (obj instanceof _) return obj;
+		if (!(this instanceof _)) return new _(obj);
+		this._wrapped = obj;
+	};
+
+	// Export the Underscore object for **Node.js**, with
+	// backwards-compatibility for the old `require()` API. If we're in
+	// the browser, add `_` as a global object.
+	//   if (typeof exports !== 'undefined') {
+	//     if (typeof module !== 'undefined' && module.exports) {
+	//       exports = module.exports = _;
+	//     }
+	//     exports._ = _;
+	//   } else {
+	//     root._ = _;
+	//   }
+	module.exports = _;
+	// Current version.
+	_.VERSION = '1.8.2';
+
+	// Internal function that returns an efficient (for current engines) version
+	// of the passed-in callback, to be repeatedly applied in other Underscore
+	// functions.
+	var optimizeCb = function (func, context, argCount) {
+		if (context === void 0) return func;
+		switch (argCount == null ? 3 : argCount) {
+			case 1: return function (value) {
+				return func.call(context, value);
+			};
+			case 2: return function (value, other) {
+				return func.call(context, value, other);
+			};
+			case 3: return function (value, index, collection) {
+				return func.call(context, value, index, collection);
+			};
+			case 4: return function (accumulator, value, index, collection) {
+				return func.call(context, accumulator, value, index, collection);
+			};
+		}
+		return function () {
+			return func.apply(context, arguments);
+		};
+	};
+
+	// A mostly-internal function to generate callbacks that can be applied
+	// to each element in a collection, returning the desired result 鈥� either
+	// identity, an arbitrary callback, a property matcher, or a property accessor.
+	var cb = function (value, context, argCount) {
+		if (value == null) return _.identity;
+		if (_.isFunction(value)) return optimizeCb(value, context, argCount);
+		if (_.isObject(value)) return _.matcher(value);
+		return _.property(value);
+	};
+	_.iteratee = function (value, context) {
+		return cb(value, context, Infinity);
+	};
+
+	// An internal function for creating assigner functions.
+	var createAssigner = function (keysFunc, undefinedOnly) {
+		return function (obj) {
+			var length = arguments.length;
+			if (length < 2 || obj == null) return obj;
+			for (var index = 1; index < length; index++) {
+				var source = arguments[index],
+					keys = keysFunc(source),
+					l = keys.length;
+				for (var i = 0; i < l; i++) {
+					var key = keys[i];
+					if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
+				}
+			}
+			return obj;
+		};
+	};
+
+	// An internal function for creating a new object that inherits from another.
+	var baseCreate = function (prototype) {
+		if (!_.isObject(prototype)) return {};
+		if (nativeCreate) return nativeCreate(prototype);
+		Ctor.prototype = prototype;
+		var result = new Ctor;
+		Ctor.prototype = null;
+		return result;
+	};
+
+	// Helper for collection methods to determine whether a collection
+	// should be iterated as an array or as an object
+	// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
+	var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
+	var isArrayLike = function (collection) {
+		var length = collection != null && collection.length;
+		return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
+	};
+
+	// Collection Functions
+	// --------------------
+
+	// The cornerstone, an `each` implementation, aka `forEach`.
+	// Handles raw objects in addition to array-likes. Treats all
+	// sparse array-likes as if they were dense.
+	_.each = _.forEach = function (obj, iteratee, context) {
+		iteratee = optimizeCb(iteratee, context);
+		var i, length;
+		if (isArrayLike(obj)) {
+			for (i = 0, length = obj.length; i < length; i++) {
+				iteratee(obj[i], i, obj);
+			}
+		} else {
+			var keys = _.keys(obj);
+			for (i = 0, length = keys.length; i < length; i++) {
+				iteratee(obj[keys[i]], keys[i], obj);
+			}
+		}
+		return obj;
+	};
+
+	// Return the results of applying the iteratee to each element.
+	_.map = _.collect = function (obj, iteratee, context) {
+		iteratee = cb(iteratee, context);
+		var keys = !isArrayLike(obj) && _.keys(obj),
+			length = (keys || obj).length,
+			results = Array(length);
+		for (var index = 0; index < length; index++) {
+			var currentKey = keys ? keys[index] : index;
+			results[index] = iteratee(obj[currentKey], currentKey, obj);
+		}
+		return results;
+	};
+
+	// Create a reducing function iterating left or right.
+	function createReduce(dir) {
+		// Optimized iterator function as using arguments.length
+		// in the main function will deoptimize the, see #1991.
+		function iterator(obj, iteratee, memo, keys, index, length) {
+			for (; index >= 0 && index < length; index += dir) {
+				var currentKey = keys ? keys[index] : index;
+				memo = iteratee(memo, obj[currentKey], currentKey, obj);
+			}
+			return memo;
+		}
+
+		return function (obj, iteratee, memo, context) {
+			iteratee = optimizeCb(iteratee, context, 4);
+			var keys = !isArrayLike(obj) && _.keys(obj),
+				length = (keys || obj).length,
+				index = dir > 0 ? 0 : length - 1;
+			// Determine the initial value if none is provided.
+			if (arguments.length < 3) {
+				memo = obj[keys ? keys[index] : index];
+				index += dir;
+			}
+			return iterator(obj, iteratee, memo, keys, index, length);
+		};
+	}
+
+	// **Reduce** builds up a single result from a list of values, aka `inject`,
+	// or `foldl`.
+	_.reduce = _.foldl = _.inject = createReduce(1);
+
+	// The right-associative version of reduce, also known as `foldr`.
+	_.reduceRight = _.foldr = createReduce(-1);
+
+	// Return the first value which passes a truth test. Aliased as `detect`.
+	_.find = _.detect = function (obj, predicate, context) {
+		var key;
+		if (isArrayLike(obj)) {
+			key = _.findIndex(obj, predicate, context);
+		} else {
+			key = _.findKey(obj, predicate, context);
+		}
+		if (key !== void 0 && key !== -1) return obj[key];
+	};
+
+	// Return all the elements that pass a truth test.
+	// Aliased as `select`.
+	_.filter = _.select = function (obj, predicate, context) {
+		var results = [];
+		predicate = cb(predicate, context);
+		_.each(obj, function (value, index, list) {
+			if (predicate(value, index, list)) results.push(value);
+		});
+		return results;
+	};
+
+	// Return all the elements for which a truth test fails.
+	_.reject = function (obj, predicate, context) {
+		return _.filter(obj, _.negate(cb(predicate)), context);
+	};
+
+	// Determine whether all of the elements match a truth test.
+	// Aliased as `all`.
+	_.every = _.all = function (obj, predicate, context) {
+		predicate = cb(predicate, context);
+		var keys = !isArrayLike(obj) && _.keys(obj),
+			length = (keys || obj).length;
+		for (var index = 0; index < length; index++) {
+			var currentKey = keys ? keys[index] : index;
+			if (!predicate(obj[currentKey], currentKey, obj)) return false;
+		}
+		return true;
+	};
+
+	// Determine if at least one element in the object matches a truth test.
+	// Aliased as `any`.
+	_.some = _.any = function (obj, predicate, context) {
+		predicate = cb(predicate, context);
+		var keys = !isArrayLike(obj) && _.keys(obj),
+			length = (keys || obj).length;
+		for (var index = 0; index < length; index++) {
+			var currentKey = keys ? keys[index] : index;
+			if (predicate(obj[currentKey], currentKey, obj)) return true;
+		}
+		return false;
+	};
+
+	// Determine if the array or object contains a given value (using `===`).
+	// Aliased as `includes` and `include`.
+	_.contains = _.includes = _.include = function (obj, target, fromIndex) {
+		if (!isArrayLike(obj)) obj = _.values(obj);
+		return _.indexOf(obj, target, typeof fromIndex == 'number' && fromIndex) >= 0;
+	};
+
+	// Invoke a method (with arguments) on every item in a collection.
+	_.invoke = function (obj, method) {
+		var args = slice.call(arguments, 2);
+		var isFunc = _.isFunction(method);
+		return _.map(obj, function (value) {
+			var func = isFunc ? method : value[method];
+			return func == null ? func : func.apply(value, args);
+		});
+	};
+
+	// Convenience version of a common use case of `map`: fetching a property.
+	_.pluck = function (obj, key) {
+		return _.map(obj, _.property(key));
+	};
+
+	// Convenience version of a common use case of `filter`: selecting only objects
+	// containing specific `key:value` pairs.
+	_.where = function (obj, attrs) {
+		return _.filter(obj, _.matcher(attrs));
+	};
+
+	// Convenience version of a common use case of `find`: getting the first object
+	// containing specific `key:value` pairs.
+	_.findWhere = function (obj, attrs) {
+		return _.find(obj, _.matcher(attrs));
+	};
+
+	// Return the maximum element (or element-based computation).
+	_.max = function (obj, iteratee, context) {
+		var result = -Infinity, lastComputed = -Infinity,
+			value, computed;
+		if (iteratee == null && obj != null) {
+			obj = isArrayLike(obj) ? obj : _.values(obj);
+			for (var i = 0, length = obj.length; i < length; i++) {
+				value = obj[i];
+				if (value > result) {
+					result = value;
+				}
+			}
+		} else {
+			iteratee = cb(iteratee, context);
+			_.each(obj, function (value, index, list) {
+				computed = iteratee(value, index, list);
+				if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+					result = value;
+					lastComputed = computed;
+				}
+			});
+		}
+		return result;
+	};
+
+	// Return the minimum element (or element-based computation).
+	_.min = function (obj, iteratee, context) {
+		var result = Infinity, lastComputed = Infinity,
+			value, computed;
+		if (iteratee == null && obj != null) {
+			obj = isArrayLike(obj) ? obj : _.values(obj);
+			for (var i = 0, length = obj.length; i < length; i++) {
+				value = obj[i];
+				if (value < result) {
+					result = value;
+				}
+			}
+		} else {
+			iteratee = cb(iteratee, context);
+			_.each(obj, function (value, index, list) {
+				computed = iteratee(value, index, list);
+				if (computed < lastComputed || computed === Infinity && result === Infinity) {
+					result = value;
+					lastComputed = computed;
+				}
+			});
+		}
+		return result;
+	};
+
+	// Shuffle a collection, using the modern version of the
+	// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher鈥揧ates_shuffle).
+	_.shuffle = function (obj) {
+		var set = isArrayLike(obj) ? obj : _.values(obj);
+		var length = set.length;
+		var shuffled = Array(length);
+		for (var index = 0, rand; index < length; index++) {
+			rand = _.random(0, index);
+			if (rand !== index) shuffled[index] = shuffled[rand];
+			shuffled[rand] = set[index];
+		}
+		return shuffled;
+	};
+
+	// Sample **n** random values from a collection.
+	// If **n** is not specified, returns a single random element.
+	// The internal `guard` argument allows it to work with `map`.
+	_.sample = function (obj, n, guard) {
+		if (n == null || guard) {
+			if (!isArrayLike(obj)) obj = _.values(obj);
+			return obj[_.random(obj.length - 1)];
+		}
+		return _.shuffle(obj).slice(0, Math.max(0, n));
+	};
+
+	// Sort the object's values by a criterion produced by an iteratee.
+	_.sortBy = function (obj, iteratee, context) {
+		iteratee = cb(iteratee, context);
+		return _.pluck(_.map(obj, function (value, index, list) {
+			return {
+				value: value,
+				index: index,
+				criteria: iteratee(value, index, list)
+			};
+		}).sort(function (left, right) {
+			var a = left.criteria;
+			var b = right.criteria;
+			if (a !== b) {
+				if (a > b || a === void 0) return 1;
+				if (a < b || b === void 0) return -1;
+			}
+			return left.index - right.index;
+		}), 'value');
+	};
+
+	// An internal function used for aggregate "group by" operations.
+	var group = function (behavior) {
+		return function (obj, iteratee, context) {
+			var result = {};
+			iteratee = cb(iteratee, context);
+			_.each(obj, function (value, index) {
+				var key = iteratee(value, index, obj);
+				behavior(result, value, key);
+			});
+			return result;
+		};
+	};
+
+	// Groups the object's values by a criterion. Pass either a string attribute
+	// to group by, or a function that returns the criterion.
+	_.groupBy = group(function (result, value, key) {
+		if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+	});
+
+	// Indexes the object's values by a criterion, similar to `groupBy`, but for
+	// when you know that your index values will be unique.
+	_.indexBy = group(function (result, value, key) {
+		result[key] = value;
+	});
+
+	// Counts instances of an object that group by a certain criterion. Pass
+	// either a string attribute to count by, or a function that returns the
+	// criterion.
+	_.countBy = group(function (result, value, key) {
+		if (_.has(result, key)) result[key]++; else result[key] = 1;
+	});
+
+	// Safely create a real, live array from anything iterable.
+	_.toArray = function (obj) {
+		if (!obj) return [];
+		if (_.isArray(obj)) return slice.call(obj);
+		if (isArrayLike(obj)) return _.map(obj, _.identity);
+		return _.values(obj);
+	};
+
+	// Return the number of elements in an object.
+	_.size = function (obj) {
+		if (obj == null) return 0;
+		return isArrayLike(obj) ? obj.length : _.keys(obj).length;
+	};
+
+	// Split a collection into two arrays: one whose elements all satisfy the given
+	// predicate, and one whose elements all do not satisfy the predicate.
+	_.partition = function (obj, predicate, context) {
+		predicate = cb(predicate, context);
+		var pass = [], fail = [];
+		_.each(obj, function (value, key, obj) {
+			(predicate(value, key, obj) ? pass : fail).push(value);
+		});
+		return [pass, fail];
+	};
+
+	// Array Functions
+	// ---------------
+
+	// Get the first element of an array. Passing **n** will return the first N
+	// values in the array. Aliased as `head` and `take`. The **guard** check
+	// allows it to work with `_.map`.
+	_.first = _.head = _.take = function (array, n, guard) {
+		if (array == null) return void 0;
+		if (n == null || guard) return array[0];
+		return _.initial(array, array.length - n);
+	};
+
+	// Returns everything but the last entry of the array. Especially useful on
+	// the arguments object. Passing **n** will return all the values in
+	// the array, excluding the last N.
+	_.initial = function (array, n, guard) {
+		return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+	};
+
+	// Get the last element of an array. Passing **n** will return the last N
+	// values in the array.
+	_.last = function (array, n, guard) {
+		if (array == null) return void 0;
+		if (n == null || guard) return array[array.length - 1];
+		return _.rest(array, Math.max(0, array.length - n));
+	};
+
+	// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+	// Especially useful on the arguments object. Passing an **n** will return
+	// the rest N values in the array.
+	_.rest = _.tail = _.drop = function (array, n, guard) {
+		return slice.call(array, n == null || guard ? 1 : n);
+	};
+
+	// Trim out all falsy values from an array.
+	_.compact = function (array) {
+		return _.filter(array, _.identity);
+	};
+
+	// Internal implementation of a recursive `flatten` function.
+	var flatten = function (input, shallow, strict, startIndex) {
+		var output = [], idx = 0;
+		for (var i = startIndex || 0, length = input && input.length; i < length; i++) {
+			var value = input[i];
+			if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
+				//flatten current level of array or arguments object
+				if (!shallow) value = flatten(value, shallow, strict);
+				var j = 0, len = value.length;
+				output.length += len;
+				while (j < len) {
+					output[idx++] = value[j++];
+				}
+			} else if (!strict) {
+				output[idx++] = value;
+			}
+		}
+		return output;
+	};
+
+	// Flatten out an array, either recursively (by default), or just one level.
+	_.flatten = function (array, shallow) {
+		return flatten(array, shallow, false);
+	};
+
+	// Return a version of the array that does not contain the specified value(s).
+	_.without = function (array) {
+		return _.difference(array, slice.call(arguments, 1));
+	};
+
+	// Produce a duplicate-free version of the array. If the array has already
+	// been sorted, you have the option of using a faster algorithm.
+	// Aliased as `unique`.
+	_.uniq = _.unique = function (array, isSorted, iteratee, context) {
+		if (array == null) return [];
+		if (!_.isBoolean(isSorted)) {
+			context = iteratee;
+			iteratee = isSorted;
+			isSorted = false;
+		}
+		if (iteratee != null) iteratee = cb(iteratee, context);
+		var result = [];
+		var seen = [];
+		for (var i = 0, length = array.length; i < length; i++) {
+			var value = array[i],
+				computed = iteratee ? iteratee(value, i, array) : value;
+			if (isSorted) {
+				if (!i || seen !== computed) result.push(value);
+				seen = computed;
+			} else if (iteratee) {
+				if (!_.contains(seen, computed)) {
+					seen.push(computed);
+					result.push(value);
+				}
+			} else if (!_.contains(result, value)) {
+				result.push(value);
+			}
+		}
+		return result;
+	};
+
+	// Produce an array that contains the union: each distinct element from all of
+	// the passed-in arrays.
+	_.union = function () {
+		return _.uniq(flatten(arguments, true, true));
+	};
+
+	// Produce an array that contains every item shared between all the
+	// passed-in arrays.
+	_.intersection = function (array) {
+		if (array == null) return [];
+		var result = [];
+		var argsLength = arguments.length;
+		for (var i = 0, length = array.length; i < length; i++) {
+			var item = array[i];
+			if (_.contains(result, item)) continue;
+			for (var j = 1; j < argsLength; j++) {
+				if (!_.contains(arguments[j], item)) break;
+			}
+			if (j === argsLength) result.push(item);
+		}
+		return result;
+	};
+
+	// Take the difference between one array and a number of other arrays.
+	// Only the elements present in just the first array will remain.
+	_.difference = function (array) {
+		var rest = flatten(arguments, true, true, 1);
+		return _.filter(array, function (value) {
+			return !_.contains(rest, value);
+		});
+	};
+
+	// Zip together multiple lists into a single array -- elements that share
+	// an index go together.
+	_.zip = function () {
+		return _.unzip(arguments);
+	};
+
+	// Complement of _.zip. Unzip accepts an array of arrays and groups
+	// each array's elements on shared indices
+	_.unzip = function (array) {
+		var length = array && _.max(array, 'length').length || 0;
+		var result = Array(length);
+
+		for (var index = 0; index < length; index++) {
+			result[index] = _.pluck(array, index);
+		}
+		return result;
+	};
+
+	// Converts lists into objects. Pass either a single array of `[key, value]`
+	// pairs, or two parallel arrays of the same length -- one of keys, and one of
+	// the corresponding values.
+	_.object = function (list, values) {
+		var result = {};
+		for (var i = 0, length = list && list.length; i < length; i++) {
+			if (values) {
+				result[list[i]] = values[i];
+			} else {
+				result[list[i][0]] = list[i][1];
+			}
+		}
+		return result;
+	};
+
+	// Return the position of the first occurrence of an item in an array,
+	// or -1 if the item is not included in the array.
+	// If the array is large and already in sort order, pass `true`
+	// for **isSorted** to use binary search.
+	_.indexOf = function (array, item, isSorted) {
+		var i = 0, length = array && array.length;
+		if (typeof isSorted == 'number') {
+			i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
+		} else if (isSorted && length) {
+			i = _.sortedIndex(array, item);
+			return array[i] === item ? i : -1;
+		}
+		if (item !== item) {
+			return _.findIndex(slice.call(array, i), _.isNaN);
+		}
+		for (; i < length; i++) if (array[i] === item) return i;
+		return -1;
+	};
+
+	_.lastIndexOf = function (array, item, from) {
+		var idx = array ? array.length : 0;
+		if (typeof from == 'number') {
+			idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
+		}
+		if (item !== item) {
+			return _.findLastIndex(slice.call(array, 0, idx), _.isNaN);
+		}
+		while (--idx >= 0) if (array[idx] === item) return idx;
+		return -1;
+	};
+
+	// Generator function to create the findIndex and findLastIndex functions
+	function createIndexFinder(dir) {
+		return function (array, predicate, context) {
+			predicate = cb(predicate, context);
+			var length = array != null && array.length;
+			var index = dir > 0 ? 0 : length - 1;
+			for (; index >= 0 && index < length; index += dir) {
+				if (predicate(array[index], index, array)) return index;
+			}
+			return -1;
+		};
+	}
+
+	// Returns the first index on an array-like that passes a predicate test
+	_.findIndex = createIndexFinder(1);
+
+	_.findLastIndex = createIndexFinder(-1);
+
+	// Use a comparator function to figure out the smallest index at which
+	// an object should be inserted so as to maintain order. Uses binary search.
+	_.sortedIndex = function (array, obj, iteratee, context) {
+		iteratee = cb(iteratee, context, 1);
+		var value = iteratee(obj);
+		var low = 0, high = array.length;
+		while (low < high) {
+			var mid = Math.floor((low + high) / 2);
+			if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+		}
+		return low;
+	};
+
+	// Generate an integer Array containing an arithmetic progression. A port of
+	// the native Python `range()` function. See
+	// [the Python documentation](http://docs.python.org/library/functions.html#range).
+	_.range = function (start, stop, step) {
+		if (arguments.length <= 1) {
+			stop = start || 0;
+			start = 0;
+		}
+		step = step || 1;
+
+		var length = Math.max(Math.ceil((stop - start) / step), 0);
+		var range = Array(length);
+
+		for (var idx = 0; idx < length; idx++ , start += step) {
+			range[idx] = start;
+		}
+
+		return range;
+	};
+
+	// Function (ahem) Functions
+	// ------------------
+
+	// Determines whether to execute a function as a constructor
+	// or a normal function with the provided arguments
+	var executeBound = function (sourceFunc, boundFunc, context, callingContext, args) {
+		if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
+		var self = baseCreate(sourceFunc.prototype);
+		var result = sourceFunc.apply(self, args);
+		if (_.isObject(result)) return result;
+		return self;
+	};
+
+	// Create a function bound to a given object (assigning `this`, and arguments,
+	// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+	// available.
+	_.bind = function (func, context) {
+		if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+		if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+		var args = slice.call(arguments, 2);
+		var bound = function () {
+			return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
+		};
+		return bound;
+	};
+
+	// Partially apply a function by creating a version that has had some of its
+	// arguments pre-filled, without changing its dynamic `this` context. _ acts
+	// as a placeholder, allowing any combination of arguments to be pre-filled.
+	_.partial = function (func) {
+		var boundArgs = slice.call(arguments, 1);
+		var bound = function () {
+			var position = 0, length = boundArgs.length;
+			var args = Array(length);
+			for (var i = 0; i < length; i++) {
+				args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
+			}
+			while (position < arguments.length) args.push(arguments[position++]);
+			return executeBound(func, bound, this, this, args);
+		};
+		return bound;
+	};
+
+	// Bind a number of an object's methods to that object. Remaining arguments
+	// are the method names to be bound. Useful for ensuring that all callbacks
+	// defined on an object belong to it.
+	_.bindAll = function (obj) {
+		var i, length = arguments.length, key;
+		if (length <= 1) throw new Error('bindAll must be passed function names');
+		for (i = 1; i < length; i++) {
+			key = arguments[i];
+			obj[key] = _.bind(obj[key], obj);
+		}
+		return obj;
+	};
+
+	// Memoize an expensive function by storing its results.
+	_.memoize = function (func, hasher) {
+		var memoize = function (key) {
+			var cache = memoize.cache;
+			var address = '' + (hasher ? hasher.apply(this, arguments) : key);
+			if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+			return cache[address];
+		};
+		memoize.cache = {};
+		return memoize;
+	};
+
+	// Delays a function for the given number of milliseconds, and then calls
+	// it with the arguments supplied.
+	_.delay = function (func, wait) {
+		var args = slice.call(arguments, 2);
+		return setTimeout(function () {
+			return func.apply(null, args);
+		}, wait);
+	};
+
+	// Defers a function, scheduling it to run after the current call stack has
+	// cleared.
+	_.defer = _.partial(_.delay, _, 1);
+
+	// Returns a function, that, when invoked, will only be triggered at most once
+	// during a given window of time. Normally, the throttled function will run
+	// as much as it can, without ever going more than once per `wait` duration;
+	// but if you'd like to disable the execution on the leading edge, pass
+	// `{leading: false}`. To disable execution on the trailing edge, ditto.
+	_.throttle = function (func, wait, options) {
+		var context, args, result;
+		var timeout = null;
+		var previous = 0;
+		if (!options) options = {};
+		var later = function () {
+			previous = options.leading === false ? 0 : _.now();
+			timeout = null;
+			result = func.apply(context, args);
+			if (!timeout) context = args = null;
+		};
+		return function () {
+			var now = _.now();
+			if (!previous && options.leading === false) previous = now;
+			var remaining = wait - (now - previous);
+			context = this;
+			args = arguments;
+			if (remaining <= 0 || remaining > wait) {
+				if (timeout) {
+					clearTimeout(timeout);
+					timeout = null;
+				}
+				previous = now;
+				result = func.apply(context, args);
+				if (!timeout) context = args = null;
+			} else if (!timeout && options.trailing !== false) {
+				timeout = setTimeout(later, remaining);
+			}
+			return result;
+		};
+	};
+
+	// Returns a function, that, as long as it continues to be invoked, will not
+	// be triggered. The function will be called after it stops being called for
+	// N milliseconds. If `immediate` is passed, trigger the function on the
+	// leading edge, instead of the trailing.
+	_.debounce = function (func, wait, immediate) {
+		var timeout, args, context, timestamp, result;
+
+		var later = function () {
+			var last = _.now() - timestamp;
+
+			if (last < wait && last >= 0) {
+				timeout = setTimeout(later, wait - last);
+			} else {
+				timeout = null;
+				if (!immediate) {
+					result = func.apply(context, args);
+					if (!timeout) context = args = null;
+				}
+			}
+		};
+
+		return function () {
+			context = this;
+			args = arguments;
+			timestamp = _.now();
+			var callNow = immediate && !timeout;
+			if (!timeout) timeout = setTimeout(later, wait);
+			if (callNow) {
+				result = func.apply(context, args);
+				context = args = null;
+			}
+
+			return result;
+		};
+	};
+
+	// Returns the first function passed as an argument to the second,
+	// allowing you to adjust arguments, run code before and after, and
+	// conditionally execute the original function.
+	_.wrap = function (func, wrapper) {
+		return _.partial(wrapper, func);
+	};
+
+	// Returns a negated version of the passed-in predicate.
+	_.negate = function (predicate) {
+		return function () {
+			return !predicate.apply(this, arguments);
+		};
+	};
+
+	// Returns a function that is the composition of a list of functions, each
+	// consuming the return value of the function that follows.
+	_.compose = function () {
+		var args = arguments;
+		var start = args.length - 1;
+		return function () {
+			var i = start;
+			var result = args[start].apply(this, arguments);
+			while (i--) result = args[i].call(this, result);
+			return result;
+		};
+	};
+
+	// Returns a function that will only be executed on and after the Nth call.
+	_.after = function (times, func) {
+		return function () {
+			if (--times < 1) {
+				return func.apply(this, arguments);
+			}
+		};
+	};
+
+	// Returns a function that will only be executed up to (but not including) the Nth call.
+	_.before = function (times, func) {
+		var memo;
+		return function () {
+			if (--times > 0) {
+				memo = func.apply(this, arguments);
+			}
+			if (times <= 1) func = null;
+			return memo;
+		};
+	};
+
+	// Returns a function that will be executed at most one time, no matter how
+	// often you call it. Useful for lazy initialization.
+	_.once = _.partial(_.before, 2);
+
+	// Object Functions
+	// ----------------
+
+	// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
+	var hasEnumBug = !{ toString: null }.propertyIsEnumerable('toString');
+	var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
+		'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
+
+	function collectNonEnumProps(obj, keys) {
+		var nonEnumIdx = nonEnumerableProps.length;
+		var constructor = obj.constructor;
+		var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
+
+		// Constructor is a special case.
+		var prop = 'constructor';
+		if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
+
+		while (nonEnumIdx--) {
+			prop = nonEnumerableProps[nonEnumIdx];
+			if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
+				keys.push(prop);
+			}
+		}
+	}
+
+	// Retrieve the names of an object's own properties.
+	// Delegates to **ECMAScript 5**'s native `Object.keys`
+	_.keys = function (obj) {
+		if (!_.isObject(obj)) return [];
+		if (nativeKeys) return nativeKeys(obj);
+		var keys = [];
+		for (var key in obj) if (_.has(obj, key)) keys.push(key);
+		// Ahem, IE < 9.
+		if (hasEnumBug) collectNonEnumProps(obj, keys);
+		return keys;
+	};
+
+	// Retrieve all the property names of an object.
+	_.allKeys = function (obj) {
+		if (!_.isObject(obj)) return [];
+		var keys = [];
+		for (var key in obj) keys.push(key);
+		// Ahem, IE < 9.
+		if (hasEnumBug) collectNonEnumProps(obj, keys);
+		return keys;
+	};
+
+	// Retrieve the values of an object's properties.
+	_.values = function (obj) {
+		var keys = _.keys(obj);
+		var length = keys.length;
+		var values = Array(length);
+		for (var i = 0; i < length; i++) {
+			values[i] = obj[keys[i]];
+		}
+		return values;
+	};
+
+	// Returns the results of applying the iteratee to each element of the object
+	// In contrast to _.map it returns an object
+	_.mapObject = function (obj, iteratee, context) {
+		iteratee = cb(iteratee, context);
+		var keys = _.keys(obj),
+			length = keys.length,
+			results = {},
+			currentKey;
+		for (var index = 0; index < length; index++) {
+			currentKey = keys[index];
+			results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
+		}
+		return results;
+	};
+
+	// Convert an object into a list of `[key, value]` pairs.
+	_.pairs = function (obj) {
+		var keys = _.keys(obj);
+		var length = keys.length;
+		var pairs = Array(length);
+		for (var i = 0; i < length; i++) {
+			pairs[i] = [keys[i], obj[keys[i]]];
+		}
+		return pairs;
+	};
+
+	// Invert the keys and values of an object. The values must be serializable.
+	_.invert = function (obj) {
+		var result = {};
+		var keys = _.keys(obj);
+		for (var i = 0, length = keys.length; i < length; i++) {
+			result[obj[keys[i]]] = keys[i];
+		}
+		return result;
+	};
+
+	// Return a sorted list of the function names available on the object.
+	// Aliased as `methods`
+	_.functions = _.methods = function (obj) {
+		var names = [];
+		for (var key in obj) {
+			if (_.isFunction(obj[key])) names.push(key);
+		}
+		return names.sort();
+	};
+
+	// Extend a given object with all the properties in passed-in object(s).
+	_.extend = createAssigner(_.allKeys);
+
+	// Assigns a given object with all the own properties in the passed-in object(s)
+	// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
+	_.extendOwn = _.assign = createAssigner(_.keys);
+
+	// Returns the first key on an object that passes a predicate test
+	_.findKey = function (obj, predicate, context) {
+		predicate = cb(predicate, context);
+		var keys = _.keys(obj), key;
+		for (var i = 0, length = keys.length; i < length; i++) {
+			key = keys[i];
+			if (predicate(obj[key], key, obj)) return key;
+		}
+	};
+
+	// Return a copy of the object only containing the whitelisted properties.
+	_.pick = function (object, oiteratee, context) {
+		var result = {}, obj = object, iteratee, keys;
+		if (obj == null) return result;
+		if (_.isFunction(oiteratee)) {
+			keys = _.allKeys(obj);
+			iteratee = optimizeCb(oiteratee, context);
+		} else {
+			keys = flatten(arguments, false, false, 1);
+			iteratee = function (value, key, obj) { return key in obj; };
+			obj = Object(obj);
+		}
+		for (var i = 0, length = keys.length; i < length; i++) {
+			var key = keys[i];
+			var value = obj[key];
+			if (iteratee(value, key, obj)) result[key] = value;
+		}
+		return result;
+	};
+
+	// Return a copy of the object without the blacklisted properties.
+	_.omit = function (obj, iteratee, context) {
+		if (_.isFunction(iteratee)) {
+			iteratee = _.negate(iteratee);
+		} else {
+			var keys = _.map(flatten(arguments, false, false, 1), String);
+			iteratee = function (value, key) {
+				return !_.contains(keys, key);
+			};
+		}
+		return _.pick(obj, iteratee, context);
+	};
+
+	// Fill in a given object with default properties.
+	_.defaults = createAssigner(_.allKeys, true);
+
+	// Creates an object that inherits from the given prototype object.
+	// If additional properties are provided then they will be added to the
+	// created object.
+	_.create = function (prototype, props) {
+		var result = baseCreate(prototype);
+		if (props) _.extendOwn(result, props);
+		return result;
+	};
+
+	// Create a (shallow-cloned) duplicate of an object.
+	_.clone = function (obj) {
+		if (!_.isObject(obj)) return obj;
+		return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+	};
+
+	// Invokes interceptor with the obj, and then returns obj.
+	// The primary purpose of this method is to "tap into" a method chain, in
+	// order to perform operations on intermediate results within the chain.
+	_.tap = function (obj, interceptor) {
+		interceptor(obj);
+		return obj;
+	};
+
+	// Returns whether an object has a given set of `key:value` pairs.
+	_.isMatch = function (object, attrs) {
+		var keys = _.keys(attrs), length = keys.length;
+		if (object == null) return !length;
+		var obj = Object(object);
+		for (var i = 0; i < length; i++) {
+			var key = keys[i];
+			if (attrs[key] !== obj[key] || !(key in obj)) return false;
+		}
+		return true;
+	};
+
+
+	// Internal recursive comparison function for `isEqual`.
+	var eq = function (a, b, aStack, bStack) {
+		// Identical objects are equal. `0 === -0`, but they aren't identical.
+		// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+		if (a === b) return a !== 0 || 1 / a === 1 / b;
+		// A strict comparison is necessary because `null == undefined`.
+		if (a == null || b == null) return a === b;
+		// Unwrap any wrapped objects.
+		if (a instanceof _) a = a._wrapped;
+		if (b instanceof _) b = b._wrapped;
+		// Compare `[[Class]]` names.
+		var className = toString.call(a);
+		if (className !== toString.call(b)) return false;
+		switch (className) {
+			// Strings, numbers, regular expressions, dates, and booleans are compared by value.
+			case '[object RegExp]':
+			// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+			case '[object String]':
+				// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+				// equivalent to `new String("5")`.
+				return '' + a === '' + b;
+			case '[object Number]':
+				// `NaN`s are equivalent, but non-reflexive.
+				// Object(NaN) is equivalent to NaN
+				if (+a !== +a) return +b !== +b;
+				// An `egal` comparison is performed for other numeric values.
+				return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+			case '[object Date]':
+			case '[object Boolean]':
+				// Coerce dates and booleans to numeric primitive values. Dates are compared by their
+				// millisecond representations. Note that invalid dates with millisecond representations
+				// of `NaN` are not equivalent.
+				return +a === +b;
+		}
+
+		var areArrays = className === '[object Array]';
+		if (!areArrays) {
+			if (typeof a != 'object' || typeof b != 'object') return false;
+
+			// Objects with different constructors are not equivalent, but `Object`s or `Array`s
+			// from different frames are.
+			var aCtor = a.constructor, bCtor = b.constructor;
+			if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+				_.isFunction(bCtor) && bCtor instanceof bCtor)
+				&& ('constructor' in a && 'constructor' in b)) {
+				return false;
+			}
+		}
+		// Assume equality for cyclic structures. The algorithm for detecting cyclic
+		// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+
+		// Initializing stack of traversed objects.
+		// It's done here since we only need them for objects and arrays comparison.
+		aStack = aStack || [];
+		bStack = bStack || [];
+		var length = aStack.length;
+		while (length--) {
+			// Linear search. Performance is inversely proportional to the number of
+			// unique nested structures.
+			if (aStack[length] === a) return bStack[length] === b;
+		}
+
+		// Add the first object to the stack of traversed objects.
+		aStack.push(a);
+		bStack.push(b);
+
+		// Recursively compare objects and arrays.
+		if (areArrays) {
+			// Compare array lengths to determine if a deep comparison is necessary.
+			length = a.length;
+			if (length !== b.length) return false;
+			// Deep compare the contents, ignoring non-numeric properties.
+			while (length--) {
+				if (!eq(a[length], b[length], aStack, bStack)) return false;
+			}
+		} else {
+			// Deep compare objects.
+			var keys = _.keys(a), key;
+			length = keys.length;
+			// Ensure that both objects contain the same number of properties before comparing deep equality.
+			if (_.keys(b).length !== length) return false;
+			while (length--) {
+				// Deep compare each member
+				key = keys[length];
+				if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
+			}
+		}
+		// Remove the first object from the stack of traversed objects.
+		aStack.pop();
+		bStack.pop();
+		return true;
+	};
+
+	// Perform a deep comparison to check if two objects are equal.
+	_.isEqual = function (a, b) {
+		return eq(a, b);
+	};
+
+	// Is a given array, string, or object empty?
+	// An "empty" object has no enumerable own-properties.
+	_.isEmpty = function (obj) {
+		if (obj == null) return true;
+		if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
+		return _.keys(obj).length === 0;
+	};
+
+	// Is a given value a DOM element?
+	_.isElement = function (obj) {
+		return !!(obj && obj.nodeType === 1);
+	};
+
+	// Is a given value an array?
+	// Delegates to ECMA5's native Array.isArray
+	_.isArray = nativeIsArray || function (obj) {
+		return toString.call(obj) === '[object Array]';
+	};
+
+	// Is a given variable an object?
+	_.isObject = function (obj) {
+		var type = typeof obj;
+		return type === 'function' || type === 'object' && !!obj;
+	};
+
+	// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
+	_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function (name) {
+		_['is' + name] = function (obj) {
+			return toString.call(obj) === '[object ' + name + ']';
+		};
+	});
+
+	// Define a fallback version of the method in browsers (ahem, IE < 9), where
+	// there isn't any inspectable "Arguments" type.
+	if (!_.isArguments(arguments)) {
+		_.isArguments = function (obj) {
+			return _.has(obj, 'callee');
+		};
+	}
+
+	// Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
+	// IE 11 (#1621), and in Safari 8 (#1929).
+	if (typeof /./ != 'function' && typeof Int8Array != 'object') {
+		_.isFunction = function (obj) {
+			return typeof obj == 'function' || false;
+		};
+	}
+
+	// Is a given object a finite number?
+	_.isFinite = function (obj) {
+		return isFinite(obj) && !isNaN(parseFloat(obj));
+	};
+
+	// Is the given value `NaN`? (NaN is the only number which does not equal itself).
+	_.isNaN = function (obj) {
+		return _.isNumber(obj) && obj !== +obj;
+	};
+
+	// Is a given value a boolean?
+	_.isBoolean = function (obj) {
+		return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+	};
+
+	// Is a given value equal to null?
+	_.isNull = function (obj) {
+		return obj === null;
+	};
+
+	// Is a given variable undefined?
+	_.isUndefined = function (obj) {
+		return obj === void 0;
+	};
+
+	// Shortcut function for checking if an object has a given property directly
+	// on itself (in other words, not on a prototype).
+	_.has = function (obj, key) {
+		return obj != null && hasOwnProperty.call(obj, key);
+	};
+
+	// Utility Functions
+	// -----------------
+
+	// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+	// previous owner. Returns a reference to the Underscore object.
+	_.noConflict = function () {
+		root._ = previousUnderscore;
+		return this;
+	};
+
+	// Keep the identity function around for default iteratees.
+	_.identity = function (value) {
+		return value;
+	};
+
+	// Predicate-generating functions. Often useful outside of Underscore.
+	_.constant = function (value) {
+		return function () {
+			return value;
+		};
+	};
+
+	_.noop = function () { };
+
+	_.property = function (key) {
+		return function (obj) {
+			return obj == null ? void 0 : obj[key];
+		};
+	};
+
+	// Generates a function for a given object that returns a given property.
+	_.propertyOf = function (obj) {
+		return obj == null ? function () { } : function (key) {
+			return obj[key];
+		};
+	};
+
+	// Returns a predicate for checking whether an object has a given set of 
+	// `key:value` pairs.
+	_.matcher = _.matches = function (attrs) {
+		attrs = _.extendOwn({}, attrs);
+		return function (obj) {
+			return _.isMatch(obj, attrs);
+		};
+	};
+
+	// Run a function **n** times.
+	_.times = function (n, iteratee, context) {
+		var accum = Array(Math.max(0, n));
+		iteratee = optimizeCb(iteratee, context, 1);
+		for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+		return accum;
+	};
+
+	// Return a random integer between min and max (inclusive).
+	_.random = function (min, max) {
+		if (max == null) {
+			max = min;
+			min = 0;
+		}
+		return min + Math.floor(Math.random() * (max - min + 1));
+	};
+
+	// A (possibly faster) way to get the current timestamp as an integer.
+	_.now = Date.now || function () {
+		return new Date().getTime();
+	};
+
+	// List of HTML entities for escaping.
+	var escapeMap = {
+		'&': '&amp;',
+		'<': '&lt;',
+		'>': '&gt;',
+		'"': '&quot;',
+		"'": '&#x27;',
+		'`': '&#x60;'
+	};
+	var unescapeMap = _.invert(escapeMap);
+
+	// Functions for escaping and unescaping strings to/from HTML interpolation.
+	var createEscaper = function (map) {
+		var escaper = function (match) {
+			return map[match];
+		};
+		// Regexes for identifying a key that needs to be escaped
+		var source = '(?:' + _.keys(map).join('|') + ')';
+		var testRegexp = RegExp(source);
+		var replaceRegexp = RegExp(source, 'g');
+		return function (string) {
+			string = string == null ? '' : '' + string;
+			return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+		};
+	};
+	_.escape = createEscaper(escapeMap);
+	_.unescape = createEscaper(unescapeMap);
+
+	// If the value of the named `property` is a function then invoke it with the
+	// `object` as context; otherwise, return it.
+	_.result = function (object, property, fallback) {
+		var value = object == null ? void 0 : object[property];
+		if (value === void 0) {
+			value = fallback;
+		}
+		return _.isFunction(value) ? value.call(object) : value;
+	};
+
+	// Generate a unique integer id (unique within the entire client session).
+	// Useful for temporary DOM ids.
+	var idCounter = 0;
+	_.uniqueId = function (prefix) {
+		var id = ++idCounter + '';
+		return prefix ? prefix + id : id;
+	};
+
+	// By default, Underscore uses ERB-style template delimiters, change the
+	// following template settings to use alternative delimiters.
+	_.templateSettings = {
+		evaluate: /<%([\s\S]+?)%>/g,
+		interpolate: /<%=([\s\S]+?)%>/g,
+		escape: /<%-([\s\S]+?)%>/g
+	};
+
+	// When customizing `templateSettings`, if you don't want to define an
+	// interpolation, evaluation or escaping regex, we need one that is
+	// guaranteed not to match.
+	var noMatch = /(.)^/;
+
+	// Certain characters need to be escaped so that they can be put into a
+	// string literal.
+	var escapes = {
+		"'": "'",
+		'\\': '\\',
+		'\r': 'r',
+		'\n': 'n',
+		'\u2028': 'u2028',
+		'\u2029': 'u2029'
+	};
+
+	var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
+
+	var escapeChar = function (match) {
+		return '\\' + escapes[match];
+	};
+
+	// JavaScript micro-templating, similar to John Resig's implementation.
+	// Underscore templating handles arbitrary delimiters, preserves whitespace,
+	// and correctly escapes quotes within interpolated code.
+	// NB: `oldSettings` only exists for backwards compatibility.
+	_.template = function (text, settings, oldSettings) {
+		if (!settings && oldSettings) settings = oldSettings;
+		settings = _.defaults({}, settings, _.templateSettings);
+
+		// Combine delimiters into one regular expression via alternation.
+		var matcher = RegExp([
+			(settings.escape || noMatch).source,
+			(settings.interpolate || noMatch).source,
+			(settings.evaluate || noMatch).source
+		].join('|') + '|$', 'g');
+
+		// Compile the template source, escaping string literals appropriately.
+		var index = 0;
+		var source = "__p+='";
+		text.replace(matcher, function (match, escape, interpolate, evaluate, offset) {
+			source += text.slice(index, offset).replace(escaper, escapeChar);
+			index = offset + match.length;
+
+			if (escape) {
+				source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+			} else if (interpolate) {
+				source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+			} else if (evaluate) {
+				source += "';\n" + evaluate + "\n__p+='";
+			}
+
+			// Adobe VMs need the match returned to produce the correct offest.
+			return match;
+		});
+		source += "';\n";
+
+		// If a variable is not specified, place data values in local scope.
+		if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+		source = "var __t,__p='',__j=Array.prototype.join," +
+			"print=function(){__p+=__j.call(arguments,'');};\n" +
+			source + 'return __p;\n';
+
+		try {
+			var render = new Function(settings.variable || 'obj', '_', source);
+		} catch (e) {
+			e.source = source;
+			throw e;
+		}
+
+		var template = function (data) {
+			return render.call(this, data, _);
+		};
+
+		// Provide the compiled source as a convenience for precompilation.
+		var argument = settings.variable || 'obj';
+		template.source = 'function(' + argument + '){\n' + source + '}';
+
+		return template;
+	};
+
+	// Add a "chain" function. Start chaining a wrapped Underscore object.
+	_.chain = function (obj) {
+		var instance = _(obj);
+		instance._chain = true;
+		return instance;
+	};
+
+	// OOP
+	// ---------------
+	// If Underscore is called as a function, it returns a wrapped object that
+	// can be used OO-style. This wrapper holds altered versions of all the
+	// underscore functions. Wrapped objects may be chained.
+
+	// Helper function to continue chaining intermediate results.
+	var result = function (instance, obj) {
+		return instance._chain ? _(obj).chain() : obj;
+	};
+
+	// Add your own custom functions to the Underscore object.
+	_.mixin = function (obj) {
+		_.each(_.functions(obj), function (name) {
+			var func = _[name] = obj[name];
+			_.prototype[name] = function () {
+				var args = [this._wrapped];
+				push.apply(args, arguments);
+				return result(this, func.apply(_, args));
+			};
+		});
+	};
+
+	// Add all of the Underscore functions to the wrapper object.
+	_.mixin(_);
+
+	// Add all mutator Array functions to the wrapper.
+	_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function (name) {
+		var method = ArrayProto[name];
+		_.prototype[name] = function () {
+			var obj = this._wrapped;
+			method.apply(obj, arguments);
+			if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+			return result(this, obj);
+		};
+	});
+
+	// Add all accessor Array functions to the wrapper.
+	_.each(['concat', 'join', 'slice'], function (name) {
+		var method = ArrayProto[name];
+		_.prototype[name] = function () {
+			return result(this, method.apply(this._wrapped, arguments));
+		};
+	});
+
+	// Extracts the result from a wrapped and chained object.
+	_.prototype.value = function () {
+		return this._wrapped;
+	};
+
+	// Provide unwrapping proxy for some methods used in engine operations
+	// such as arithmetic and JSON stringification.
+	_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
+
+	_.prototype.toString = function () {
+		return '' + this._wrapped;
+	};
+
+	// AMD registration happens at the end for compatibility with AMD loaders
+	// that may not enforce next-turn semantics on modules. Even though general
+	// practice for AMD registration is to be anonymous, underscore registers
+	// as a named module because, like jQuery, it is a base library that is
+	// popular enough to be bundled in a third party lib, but not be part of
+	// an AMD load request. Those cases could generate an error when an
+	// anonymous define() is called outside of a loader request.
+	//   if (typeof define === 'function' && define.amd) {
+	//     define('underscore', [], function() {
+	//       return _;
+	//     });
+	//   }
+}.call(this));

+ 1022 - 0
common/we7_js/util.js

@@ -0,0 +1,1022 @@
+import {
+	base64_encode,
+	base64_decode
+} from './base64.js';
+import md5 from './md5.js';
+import App from '../../App'
+var util = {};
+
+util.base64Encode = function(str) {
+	return base64_encode(str)
+};
+
+util.base64Decode = function(str) {
+	return base64_decode(str)
+};
+
+util.md5 = function(str) {
+	return md5(str)
+};
+
+
+/**
+	构造微擎地址, 
+	@params action 微擎系统中的controller, action, do,格式为 'wxapp/home/navs'
+	@params querystring 格式为 {参数名1 : 值1, 参数名2 : 值2}
+*/
+util.url = function(action, querystring) {
+	// console.log('这是util下',App)
+	var app = App;
+	var formvar = 'wxapp';
+	var url = '';
+
+	//#ifdef H5
+	var ua = navigator.userAgent.toLowerCase();
+	var isWeixin = ua.indexOf('micromessenger') != -1;
+
+	if (isWeixin) {
+		formvar = 'mp';
+	} else {
+		formvar = 'h5';
+	}
+	//#endif
+
+	//#ifdef APP-PLUS
+	formvar = 'app';
+	//#endif
+
+	url = app.siteInfo.siteroot + '?i=' + app.siteInfo.uniacid + '&t=' + app.siteInfo.multiid + '&v=' + app
+		.siteInfo.version + '&from=' + formvar + '&';
+
+	//#ifdef H5
+	let urlquery = getQuery(window.location.href);
+	if (urlquery.length > 0) {
+		var urli = '';
+		for (let i = 0; i < urlquery.length; i++) {
+			if (urlquery[i] && urlquery[i].name && urlquery[i].value) {
+				if (urlquery[i].name == "i") {
+					urli = urlquery[i].name + '=' + urlquery[i].value;
+				}
+			}
+		}
+		if (urli) {
+			url = window.location.protocol + '//' + window.location.host + '/app/index.php?t=' + app.siteInfo
+				.multiid + '&v=' + app
+				.siteInfo
+				.version + '&from=' + formvar + '&' + urli + '&';
+		}
+	}
+	//#endif
+	if (action) {
+		action = action.split('/');
+		if (action[0]) {
+			url += 'c=' + action[0] + '&';
+		}
+		if (action[1]) {
+			url += 'a=' + action[1] + '&';
+		}
+		if (action[2]) {
+			url += 'do=' + action[2] + '&';
+		}
+	}
+
+	if (querystring && typeof querystring === 'object') {
+		for (let param in querystring) {
+			if (param && querystring.hasOwnProperty(param) && querystring[param]) {
+				url += param + '=' + querystring[param] + '&';
+			}
+		}
+	}
+	//console.log(url);
+	return url;
+}
+
+function getQuery(url) {
+	var theRequest = [];
+	if (url.indexOf("?") != -1) {
+		var str = url.split('?')[1];
+		if (str.indexOf("#") != -1) {
+			str = str.split('#')[0]
+		}
+		var strs = str.split("&");
+		for (var i = 0; i < strs.length; i++) {
+			if (strs[i].split("=")[0] && unescape(strs[i].split("=")[1])) {
+				theRequest[i] = {
+					'name': strs[i].split("=")[0],
+					'value': unescape(strs[i].split("=")[1])
+				}
+			}
+		}
+	}
+	return theRequest;
+}
+/*
+ * 获取链接某个参数
+ * url 链接地址
+ * name 参数名称
+ */
+function getUrlParam(url, name) {
+	var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象  
+	var r = url.split('?')[1].match(reg); //匹配目标参数  
+	if (r != null) return unescape(r[2]);
+	return null; //返回参数值  
+}
+/**
+ * 获取签名 将链接地址的所有参数按字母排序后拼接加上token进行md5
+ * url 链接地址
+ * date 参数{参数名1 : 值1, 参数名2 : 值2} *
+ * token 签名token 非必须
+ */
+function getSign(url, data, token) {
+	var _ = require('./underscore.js');
+	var md5 = require('./md5.js');
+	var querystring = '';
+	var sign = getUrlParam(url, 'sign');
+	if (sign || (data && data.sign)) {
+		return false;
+	} else {
+		if (url) {
+			querystring = getQuery(url);
+		}
+		if (data) {
+			var theRequest = [];
+			for (let param in data) {
+				if (param && data[param]) {
+					theRequest = theRequest.concat({
+						'name': param,
+						'value': data[param]
+					})
+				}
+			}
+			querystring = querystring.concat(theRequest);
+		}
+		//排序
+		querystring = _.sortBy(querystring, 'name');
+		//去重
+		querystring = _.uniq(querystring, true, 'name');
+		var urlData = '';
+		for (let i = 0; i < querystring.length; i++) {
+			if (querystring[i] && querystring[i].name && querystring[i].value) {
+				urlData += querystring[i].name + '=' + querystring[i].value;
+				if (i < (querystring.length - 1)) {
+					urlData += '&';
+				}
+			}
+		}
+		token = token ? token : App.siteInfo.token;
+		sign = md5(urlData + token);
+		return sign;
+	}
+}
+util.getSign = function(url, data, token) {
+	return getSign(url, data, token);
+};
+/**
+	二次封装微信wx.request函数、增加交互体全、配置缓存、以及配合微擎格式化返回数据
+*/
+util.request = function(option) {
+	var _ = require('./underscore.js');
+	var md5 = require('./md5.js');
+	var app = App;
+	var option = option ? option : {};
+	option.cachetime = option.cachetime ? option.cachetime : 0;
+	//console.log(option.showLoading);
+	//option.showLoading = typeof option.showLoading != 'undefined' ? option.showLoading : true;
+	if (typeof option.showLoading == 'undefined') {
+		option.showLoading = false;
+	}
+	//console.log(option.showLoading);
+	var sessionid = uni.getStorageSync('userInfo').sessionid;
+
+	var url = option.url;
+	if (url.indexOf('http://') == -1 && url.indexOf('https://') == -1) {
+		url = util.url(url);
+	}
+	var state = getUrlParam(url, 'state');
+
+	if (!state && !(option.data && option.data.state) && sessionid) {
+
+		url = url + '&state=we7sid-' + sessionid
+	}
+	if (!option.data || !option.data.m) {
+		if (App.module) {
+			url = url + '&m=' + App.module;
+		} else {
+			var nowPage = getCurrentPages();
+			if (nowPage.length) {
+				nowPage = nowPage[getCurrentPages().length - 1];
+				if (nowPage && nowPage.__route__) {
+					url = url + '&m=' + nowPage.__route__.split('/')[0];
+				}
+			}
+		}
+	}
+
+	var sign = getSign(url, option.data);
+	if (sign) {
+		url = url + "&sign=" + sign;
+	}
+	if (!url) {
+		return false;
+	}
+
+	if (option.showLoading) {
+		//uni.showNavigationBarLoading();
+		util.showLoading();
+	}
+	if (option.cachetime) {
+		var cachekey = md5(url);
+		var cachedata = uni.getStorageSync(cachekey);
+		var timestamp = (new Date()).valueOf();
+
+		if (cachedata && cachedata.data) {
+			if (cachedata.expire > timestamp) {
+				if (option.complete && typeof option.complete == 'function') {
+					option.complete(cachedata);
+				}
+				if (option.success && typeof option.success == 'function') {
+					option.success(cachedata);
+				}
+				//console.log('cache:' + url);
+				uni.hideLoading();
+				uni.hideNavigationBarLoading();
+				return true;
+			} else {
+				uni.removeStorageSync(cachekey)
+			}
+		}
+	}
+	console.log(url);
+	uni.request({
+		'url': url,
+		'data': option.data ? option.data : {},
+		'header': option.header ? option.header : {},
+		'method': option.method ? option.method : 'GET',
+		'header': {
+			'content-type': 'application/x-www-form-urlencoded'
+		},
+		'success': function(response) {
+			uni.hideNavigationBarLoading();
+			uni.hideLoading();
+			if (response.data.errno) {
+				if (response.data.errno == '41009') {
+					uni.setStorageSync('userInfo', '');
+					util.getUserInfo(function() {
+						util.request(option)
+					});
+					return;
+				} else {
+					if (option.fail && typeof option.fail == 'function') {
+						option.fail(response);
+					} else {
+						if (!response.data.message) {
+							response.data.message = response.data.msg;
+						}
+						if (response.data.message) {
+							if (response.data.data != null && response.data.data.redirect) {
+								var redirect = response.data.data.redirect;
+							} else {
+								var redirect = '';
+							}
+							app.util.message(response.data.message, redirect, 'error');
+						}
+					}
+					return;
+				}
+			} else {
+				if (option.success && typeof option.success == 'function') {
+					option.success(response);
+				}
+				//写入缓存,减少HTTP请求,并且如果网络异常可以读取缓存数据
+				if (option.cachetime) {
+					var cachedata = {
+						'data': response.data,
+						'expire': timestamp + option.cachetime * 1000
+					};
+					var iscache = 1;
+					if (option.data) {
+						if (option.data.samkey) {
+							iscache = 0;
+						}
+					}
+					if (iscache==1) {
+						uni.setStorageSync(cachekey, cachedata);
+					}
+				}
+			}
+		},
+		'fail': function(response) {
+			uni.hideNavigationBarLoading();
+			uni.hideLoading();
+
+			//如果请求失败,尝试从缓存中读取数据
+			var md5 = require('./md5.js');
+			var cachekey = md5(url);
+			var cachedata = uni.getStorageSync(cachekey);
+			if (cachedata && cachedata.data) {
+				if (option.success && typeof option.success == 'function') {
+					option.success(cachedata);
+				}
+				console.log('failreadcache:' + url);
+				return true;
+			} else {
+				if (option.fail && typeof option.fail == 'function') {
+					option.fail(response);
+				}
+			}
+		},
+		'complete': function(response) {
+			// uni.hideNavigationBarLoading();
+			// uni.hideLoading();
+			if (option.complete && typeof option.complete == 'function') {
+				option.complete(response);
+			}
+		}
+	});
+}
+util.getWe7User = function(cb, code) {
+	var userInfo = uni.getStorageSync('userInfo') || {};
+	util.request({
+		url: 'auth/session/openid',
+		data: {
+			code: code ? code : ''
+		},
+		cachetime: 0,
+		showLoading: false,
+		success: function(session) {
+			if (!session.data.errno) {
+				userInfo.sessionid = session.data.data.sessionid
+				userInfo.memberInfo = session.data.data.userinfo
+				uni.setStorageSync('userInfo', userInfo)
+			}
+			typeof cb == "function" && cb(userInfo);
+		}
+	});
+}
+util.upadteUser = function(wxInfo, cb) {
+
+	var userInfo = uni.getStorageSync('userInfo');
+	if (!wxInfo) {
+		return typeof cb == "function" && cb(userInfo);;
+	}
+	userInfo.wxInfo = wxInfo.userInfo
+	util.request({
+		url: 'auth/session/userinfo',
+		data: {
+			signature: wxInfo.signature,
+			rawData: wxInfo.rawData,
+			iv: wxInfo.iv,
+			encryptedData: wxInfo.encryptedData
+		},
+		method: 'POST',
+		header: {
+			'content-type': 'application/x-www-form-urlencoded'
+		},
+		cachetime: 0,
+		success: function(res) {
+			if (!res.data.errno) {
+				userInfo.memberInfo = res.data.data;
+				uni.setStorageSync('userInfo', userInfo);
+			}
+			typeof cb == "function" && cb(userInfo);
+		}
+	});
+}
+util.checkSession = function(option) {
+	util.request({
+		url: 'auth/session/check',
+		method: 'POST',
+		cachetime: 0,
+		showLoading: false,
+		success: function(res) {
+			if (!res.data.errno) {
+				typeof option.success == "function" && option.success();
+			} else {
+				typeof option.fail == "function" && option.fail();
+			}
+		},
+		fail: function() {
+			typeof option.fail == "function" && option.fail();
+		}
+	})
+}
+/*
+ * 获取用户信息
+ */
+util.getUserInfo = function(cb, wxInfo) {
+	// #ifdef MP-WEIXIN
+	var login = function() {
+
+		var userInfo = {
+			'sessionid': '',
+			'wxInfo': '',
+			'memberInfo': '',
+		};
+
+		uni.login({
+			success: function(res) {
+				util.getWe7User(function(userInfo) {
+
+					if (wxInfo) {
+						util.upadteUser(wxInfo, function(userInfo) {
+							typeof cb == "function" && cb(userInfo);
+						})
+					} else {
+						if (uni.canIUse('getUserInfo')) {
+							// 如果可用
+							uni.getUserInfo({
+								withCredentials: true,
+								success: function(wxInfo) {
+									//console.log(wxInfo);
+									util.upadteUser(wxInfo, function(userInfo) {
+										typeof cb == "function" && cb(
+											userInfo);
+									})
+								},
+								fail: function() {
+									typeof cb == "function" && cb(userInfo);
+								}
+							})
+						} else {
+							typeof cb == "function" && cb(userInfo);
+						}
+					}
+				}, res.code)
+			},
+			fail: function(res) {
+				console.log(res);
+			}
+		});
+	};
+
+	var userInfo = uni.getStorageSync('userInfo') || {};
+	if (userInfo.sessionid) {
+		util.checkSession({
+			success: function() {
+				if (wxInfo) {
+					util.upadteUser(wxInfo, function(userInfo) {
+						typeof cb == "function" && cb(userInfo);
+					})
+				} else {
+					typeof cb == "function" && cb(userInfo);
+				}
+			},
+			fail: function() {
+				userInfo.sessionid = '';
+				console.log('relogin');
+				uni.removeStorageSync('userInfo');
+				login();
+			}
+		})
+	} else {
+		//调用登录接口
+		login();
+	}
+	// #endif
+
+	// #ifndef MP-WEIXIN
+	var mplogin = function() {
+		var userInfo = {
+			'sessionid': '',
+			'wxInfo': '',
+			'memberInfo': '',
+		};
+		util.getWe7User(function(userInfo) {
+			if (navigator) {
+				var ua = navigator.userAgent.toLowerCase();
+				var isWeixin = ua.indexOf('micromessenger') != -1;
+			}
+
+			if (isWeixin) {
+				let urlquery = getQuery(window.location.href);
+				if (urlquery.length > 0) {
+					var urli = '';
+					for (let i = 0; i < urlquery.length; i++) {
+						if (urlquery[i] && urlquery[i].name && urlquery[i].value) {
+							if (urlquery[i].name == "i") {
+								urli = urlquery[i].name + '=' + urlquery[i].value;
+							}
+						}
+					}
+				}
+
+				window.location.href =
+					'/public/index.php?s=/index/wechatmp/wechat&xmtoken=' +
+					userInfo.sessionid +
+					'&' + urli + '&backurl=' + encodeURIComponent(location.href)
+			}
+			typeof cb == "function" && cb(userInfo);
+		}, '')
+
+	};
+
+	var userInfo = uni.getStorageSync('userInfo') || {};
+	if (userInfo.sessionid) {
+		util.checkSession({
+			success: function() {
+				typeof cb == "function" && cb(userInfo);
+			},
+			fail: function() {
+				userInfo.sessionid = '';
+				console.log('relogin');
+				uni.removeStorageSync('userInfo');
+				mplogin();
+			}
+		})
+	} else {
+		//调用登录接口
+		mplogin();
+	}
+
+	// #endif
+
+}
+
+util.getmpuserinfo = function() {
+	util.getUserInfo(function(userInfo) {
+
+		var ua = navigator.userAgent.toLowerCase();
+		var isWeixin = ua.indexOf('micromessenger') != -1;
+
+		if (isWeixin) {
+			let urlquery = getQuery(window.location.href);
+			if (urlquery.length > 0) {
+				var urli = '';
+				for (let i = 0; i < urlquery.length; i++) {
+					if (urlquery[i] && urlquery[i].name && urlquery[i].value) {
+						if (urlquery[i].name == "i") {
+							urli = urlquery[i].name + '=' + urlquery[i].value;
+						}
+					}
+				}
+			}
+
+			window.location.href =
+				'/public/index.php?s=/index/wechatmp/wechatuserinfo&xmtoken=' +
+				userInfo.sessionid +
+				'&' + urli + '&backurl=' + encodeURIComponent(location.href)
+		}
+	})
+};
+
+util.navigateBack = function(obj) {
+	let delta = obj.delta ? obj.delta : 1;
+	if (obj.data) {
+		let pages = getCurrentPages()
+		let curPage = pages[pages.length - (delta + 1)];
+		if (curPage.pageForResult) {
+			curPage.pageForResult(obj.data);
+		} else {
+			curPage.setData(obj.data);
+		}
+	}
+	uni.navigateBack({
+		delta: delta, // 回退前 delta(默认为1) 页面
+		success: function(res) {
+			// success
+			typeof obj.success == "function" && obj.success(res);
+		},
+		fail: function(err) {
+			// fail
+			typeof obj.fail == "function" && obj.fail(err);
+		},
+		complete: function() {
+			// complete
+			typeof obj.complete == "function" && obj.complete();
+		}
+	})
+};
+
+util.footer = function($this) {
+	let app = App;
+	let that = $this;
+	let tabBar = app.tabBar;
+	for (let i in tabBar['list']) {
+		tabBar['list'][i]['pageUrl'] = tabBar['list'][i]['pagePath'].replace(/(\?|#)[^"]*/g, '')
+	}
+	that.tabBar = tabBar;
+
+	// #ifdef MP-WEIXIN
+	that.tabBar.thisurl = that.__route__;
+	// #endif
+	//#ifdef H5
+	that.tabBar.thisurl = that.$route.path;
+	//#endif
+
+
+};
+/*
+ * 提示信息
+ * type 为 success, error 当为 success,  时,为toast方式,否则为模态框的方式
+ * redirect 为提示后的跳转地址, 跳转的时候可以加上 协议名称  
+ * navigate:/we7/pages/detail/detail 以 navigateTo 的方法跳转,
+ * redirect:/we7/pages/detail/detail 以 redirectTo 的方式跳转,默认为 redirect
+ */
+util.message = function(title, redirect, type) {
+	if (!title) {
+		return true;
+	}
+	if (typeof title == 'object') {
+		redirect = title.redirect;
+		type = title.type;
+		title = title.title;
+	}
+	if (redirect) {
+		var redirectType = redirect.substring(0, 9),
+			url = '',
+			redirectFunction = '';
+		if (redirectType == 'navigate:') {
+			redirectFunction = 'navigateTo';
+			url = redirect.substring(9);
+		} else if (redirectType == 'redirect:') {
+			redirectFunction = 'redirectTo';
+			url = redirect.substring(9);
+		} else {
+			url = redirect;
+			redirectFunction = 'redirectTo';
+		}
+	}
+	//console.log(url)
+	if (!type) {
+		type = 'success';
+	}
+
+	if (type == 'success') {
+		uni.showToast({
+			title: title,
+			icon: 'success',
+			duration: 2000,
+			mask: url ? true : false,
+			complete: function() {
+				if (url) {
+					setTimeout(function() {
+						wx[redirectFunction]({
+							url: url,
+						});
+					}, 1800);
+				}
+
+			}
+		});
+	} else if (type == 'error') {
+		uni.showModal({
+			title: '系统信息',
+			content: title,
+			showCancel: false,
+			complete: function() {
+				if (url) {
+					wx[redirectFunction]({
+						url: url,
+					});
+				}
+			}
+		});
+	}
+}
+
+//util.user = util.getUserInfo;
+
+//封装微信等待提示,防止ajax过多时,show多次
+util.showLoading = function() {
+	var isShowLoading = uni.getStorageSync('isShowLoading');
+	if (isShowLoading) {
+		uni.hideLoading();
+		uni.setStorageSync('isShowLoading', false);
+	}
+
+	uni.showLoading({
+		title: '加载中',
+		complete: function() {
+			uni.setStorageSync('isShowLoading', true);
+		},
+		fail: function() {
+			uni.setStorageSync('isShowLoading', false);
+		}
+	});
+}
+
+util.showImage = function(event) {
+	var url = event ? event.currentTarget.dataset.preview : '';
+	if (!url) {
+		return false;
+	}
+	uni.previewImage({
+		urls: [url]
+	});
+}
+
+/**
+ * 转换内容中的emoji表情为 unicode 码点,在Php中使用utf8_bytes来转换输出
+ */
+util.parseContent = function(string) {
+	if (!string) {
+		return string;
+	}
+
+	var ranges = [
+		'\ud83c[\udf00-\udfff]', // U+1F300 to U+1F3FF
+		'\ud83d[\udc00-\ude4f]', // U+1F400 to U+1F64F
+		'\ud83d[\ude80-\udeff]' // U+1F680 to U+1F6FF
+	];
+	var emoji = string.match(
+		new RegExp(ranges.join('|'), 'g'));
+
+	if (emoji) {
+		for (var i in emoji) {
+			string = string.replace(emoji[i], '[U+' + emoji[i].codePointAt(0).toString(16).toUpperCase() + ']');
+		}
+	}
+	return string;
+}
+
+util.date = function() {
+	/**
+	 * 判断闰年
+	 * @param date Date日期对象
+	 * @return boolean true 或false
+	 */
+	this.isLeapYear = function(date) {
+		return (0 == date.getYear() % 4 && ((date.getYear() % 100 != 0) || (date.getYear() % 400 == 0)));
+	}
+
+	/**
+	 * 日期对象转换为指定格式的字符串
+	 * @param f 日期格式,格式定义如下 yyyy-MM-dd HH:mm:ss
+	 * @param date Date日期对象, 如果缺省,则为当前时间
+	 *
+	 * YYYY/yyyy/YY/yy 表示年份  
+	 * MM/M 月份  
+	 * W/w 星期  
+	 * dd/DD/d/D 日期  
+	 * hh/HH/h/H 时间  
+	 * mm/m 分钟  
+	 * ss/SS/s/S 秒  
+	 * @return string 指定格式的时间字符串
+	 */
+	this.dateToStr = function(formatStr, date) {
+		formatStr = arguments[0] || "yyyy-MM-dd HH:mm:ss";
+		date = arguments[1] || new Date();
+		var str = formatStr;
+		var Week = ['日', '一', '二', '三', '四', '五', '六'];
+		str = str.replace(/yyyy|YYYY/, date.getFullYear());
+		str = str.replace(/yy|YY/, (date.getYear() % 100) > 9 ? (date.getYear() % 100).toString() : '0' + (date
+			.getYear() % 100));
+		str = str.replace(/MM/, date.getMonth() > 9 ? (date.getMonth() + 1) : '0' + (date.getMonth() + 1));
+		str = str.replace(/M/g, date.getMonth());
+		str = str.replace(/w|W/g, Week[date.getDay()]);
+
+		str = str.replace(/dd|DD/, date.getDate() > 9 ? date.getDate().toString() : '0' + date.getDate());
+		str = str.replace(/d|D/g, date.getDate());
+
+		str = str.replace(/hh|HH/, date.getHours() > 9 ? date.getHours().toString() : '0' + date.getHours());
+		str = str.replace(/h|H/g, date.getHours());
+		str = str.replace(/mm/, date.getMinutes() > 9 ? date.getMinutes().toString() : '0' + date.getMinutes());
+		str = str.replace(/m/g, date.getMinutes());
+
+		str = str.replace(/ss|SS/, date.getSeconds() > 9 ? date.getSeconds().toString() : '0' + date
+			.getSeconds());
+		str = str.replace(/s|S/g, date.getSeconds());
+
+		return str;
+	}
+
+
+	/**
+	 * 日期计算  
+	 * @param strInterval string  可选值 y 年 m月 d日 w星期 ww周 h时 n分 s秒  
+	 * @param num int
+	 * @param date Date 日期对象
+	 * @return Date 返回日期对象
+	 */
+	this.dateAdd = function(strInterval, num, date) {
+		date = arguments[2] || new Date();
+		switch (strInterval) {
+			case 's':
+				return new Date(date.getTime() + (1000 * num));
+			case 'n':
+				return new Date(date.getTime() + (60000 * num));
+			case 'h':
+				return new Date(date.getTime() + (3600000 * num));
+			case 'd':
+				return new Date(date.getTime() + (86400000 * num));
+			case 'w':
+				return new Date(date.getTime() + ((86400000 * 7) * num));
+			case 'm':
+				return new Date(date.getFullYear(), (date.getMonth()) + num, date.getDate(), date.getHours(),
+					date.getMinutes(), date.getSeconds());
+			case 'y':
+				return new Date((date.getFullYear() + num), date.getMonth(), date.getDate(), date.getHours(),
+					date.getMinutes(), date.getSeconds());
+		}
+	}
+
+	/**
+	 * 比较日期差 dtEnd 格式为日期型或者有效日期格式字符串
+	 * @param strInterval string  可选值 y 年 m月 d日 w星期 ww周 h时 n分 s秒  
+	 * @param dtStart Date  可选值 y 年 m月 d日 w星期 ww周 h时 n分 s秒
+	 * @param dtEnd Date  可选值 y 年 m月 d日 w星期 ww周 h时 n分 s秒 
+	 */
+	this.dateDiff = function(strInterval, dtStart, dtEnd) {
+		switch (strInterval) {
+			case 's':
+				return parseInt((dtEnd - dtStart) / 1000);
+			case 'n':
+				return parseInt((dtEnd - dtStart) / 60000);
+			case 'h':
+				return parseInt((dtEnd - dtStart) / 3600000);
+			case 'd':
+				return parseInt((dtEnd - dtStart) / 86400000);
+			case 'w':
+				return parseInt((dtEnd - dtStart) / (86400000 * 7));
+			case 'm':
+				return (dtEnd.getMonth() + 1) + ((dtEnd.getFullYear() - dtStart.getFullYear()) * 12) - (dtStart
+					.getMonth() + 1);
+			case 'y':
+				return dtEnd.getFullYear() - dtStart.getFullYear();
+		}
+	}
+
+	/**
+	 * 字符串转换为日期对象 // eval 不可用
+	 * @param date Date 格式为yyyy-MM-dd HH:mm:ss,必须按年月日时分秒的顺序,中间分隔符不限制
+	 */
+	this.strToDate = function(dateStr) {
+		var data = dateStr;
+		var reCat = /(\d{1,4})/gm;
+		var t = data.match(reCat);
+		t[1] = t[1] - 1;
+		eval('var d = new Date(' + t.join(',') + ');');
+		return d;
+	}
+
+	/**
+	 * 把指定格式的字符串转换为日期对象yyyy-MM-dd HH:mm:ss
+	 * 
+	 */
+	this.strFormatToDate = function(formatStr, dateStr) {
+		var year = 0;
+		var start = -1;
+		var len = dateStr.length;
+		if ((start = formatStr.indexOf('yyyy')) > -1 && start < len) {
+			year = dateStr.substr(start, 4);
+		}
+		var month = 0;
+		if ((start = formatStr.indexOf('MM')) > -1 && start < len) {
+			month = parseInt(dateStr.substr(start, 2)) - 1;
+		}
+		var day = 0;
+		if ((start = formatStr.indexOf('dd')) > -1 && start < len) {
+			day = parseInt(dateStr.substr(start, 2));
+		}
+		var hour = 0;
+		if (((start = formatStr.indexOf('HH')) > -1 || (start = formatStr.indexOf('hh')) > 1) && start < len) {
+			hour = parseInt(dateStr.substr(start, 2));
+		}
+		var minute = 0;
+		if ((start = formatStr.indexOf('mm')) > -1 && start < len) {
+			minute = dateStr.substr(start, 2);
+		}
+		var second = 0;
+		if ((start = formatStr.indexOf('ss')) > -1 && start < len) {
+			second = dateStr.substr(start, 2);
+		}
+		return new Date(year, month, day, hour, minute, second);
+	}
+
+
+	/**
+	 * 日期对象转换为毫秒数
+	 */
+	this.dateToLong = function(date) {
+		return date.getTime();
+	}
+
+	/**
+	 * 毫秒转换为日期对象
+	 * @param dateVal number 日期的毫秒数 
+	 */
+	this.longToDate = function(dateVal) {
+		return new Date(dateVal);
+	}
+
+	/**
+	 * 判断字符串是否为日期格式
+	 * @param str string 字符串
+	 * @param formatStr string 日期格式, 如下 yyyy-MM-dd
+	 */
+	this.isDate = function(str, formatStr) {
+		if (formatStr == null) {
+			formatStr = "yyyyMMdd";
+		}
+		var yIndex = formatStr.indexOf("yyyy");
+		if (yIndex == -1) {
+			return false;
+		}
+		var year = str.substring(yIndex, yIndex + 4);
+		var mIndex = formatStr.indexOf("MM");
+		if (mIndex == -1) {
+			return false;
+		}
+		var month = str.substring(mIndex, mIndex + 2);
+		var dIndex = formatStr.indexOf("dd");
+		if (dIndex == -1) {
+			return false;
+		}
+		var day = str.substring(dIndex, dIndex + 2);
+		if (!isNumber(year) || year > "2100" || year < "1900") {
+			return false;
+		}
+		if (!isNumber(month) || month > "12" || month < "01") {
+			return false;
+		}
+		if (day > getMaxDay(year, month) || day < "01") {
+			return false;
+		}
+		return true;
+	}
+
+	this.getMaxDay = function(year, month) {
+		if (month == 4 || month == 6 || month == 9 || month == 11)
+			return "30";
+		if (month == 2)
+			if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
+				return "29";
+			else
+				return "28";
+		return "31";
+	}
+	/**
+	 *	变量是否为数字
+	 */
+	this.isNumber = function(str) {
+		var regExp = /^\d+$/g;
+		return regExp.test(str);
+	}
+
+	/**
+	 * 把日期分割成数组 [年、月、日、时、分、秒]
+	 */
+	this.toArray = function(myDate) {
+		myDate = arguments[0] || new Date();
+		var myArray = Array();
+		myArray[0] = myDate.getFullYear();
+		myArray[1] = myDate.getMonth();
+		myArray[2] = myDate.getDate();
+		myArray[3] = myDate.getHours();
+		myArray[4] = myDate.getMinutes();
+		myArray[5] = myDate.getSeconds();
+		return myArray;
+	}
+
+	/**
+	 * 取得日期数据信息  
+	 * 参数 interval 表示数据类型  
+	 * y 年 M月 d日 w星期 ww周 h时 n分 s秒  
+	 */
+	this.datePart = function(interval, myDate) {
+		myDate = arguments[1] || new Date();
+		var partStr = '';
+		var Week = ['日', '一', '二', '三', '四', '五', '六'];
+		switch (interval) {
+			case 'y':
+				partStr = myDate.getFullYear();
+				break;
+			case 'M':
+				partStr = myDate.getMonth() + 1;
+				break;
+			case 'd':
+				partStr = myDate.getDate();
+				break;
+			case 'w':
+				partStr = Week[myDate.getDay()];
+				break;
+			case 'ww':
+				partStr = myDate.WeekNumOfYear();
+				break;
+			case 'h':
+				partStr = myDate.getHours();
+				break;
+			case 'm':
+				partStr = myDate.getMinutes();
+				break;
+			case 's':
+				partStr = myDate.getSeconds();
+				break;
+		}
+		return partStr;
+	}
+
+	/**
+	 * 取得当前日期所在月的最大天数  
+	 */
+	this.maxDayOfDate = function(date) {
+		date = arguments[0] || new Date();
+		date.setDate(1);
+		date.setMonth(date.getMonth() + 1);
+		var time = date.getTime() - 24 * 60 * 60 * 1000;
+		var newDate = new Date(time);
+		return newDate.getDate();
+	}
+};
+
+module.exports = util;

+ 232 - 0
components/Pengpai-FadeInOut/Pengpai-FadeInOut.vue

@@ -0,0 +1,232 @@
+<template>
+	<view>
+		<view
+			ref="ani"
+			:animation="animationData"
+			class="message"
+			:style="{ top: top + 'px', left: left + 'px' }"
+			v-if="show"
+		>
+			<view class="round bg-gradual-orange flex justify-start shadow" style="padding: 3px;">
+				<view class="cu-avatar cu-a-sm round" :style="{ backgroundImage: `url(${info.touxiang || '/static/images/my/mine_def_touxiang_3x.png'})` }">
+					<!-- #ifdef APP-NVUE -->
+					<image :src="info.touxiang || '/static/images/my/mine_def_touxiang_3x.png'" class="avatarimg"></image>
+					<!-- #endif -->
+				</view>
+				<view class="padding-lr-sm flex align-center">
+					<text class="text-sm">{{ info.content }}</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+// #ifdef APP-NVUE
+const animation = uni.requireNativePlugin('animation');
+// #endif
+export default {
+	name: 'Pengpai-FadeInOut',
+	props: {
+		//持续时间
+		duration: {
+			type: Number,
+			default: 2600
+		},
+		//停留时间
+		wait: {
+			type: Number,
+			default: 3000
+		},
+		//顶部距离px
+		top: {
+			type: Number,
+			default: 160
+		},
+		//左边距离px
+		left: {
+			type: Number,
+			default: 10
+		},
+		//动画半径
+		radius: {
+			type: Number,
+			default: 30
+		},
+		//数据
+		info: {
+			type: [Array,Object],
+			default: () => {
+				return []
+			}
+		}
+	},
+	data() {
+		return {
+			animationData: {},
+			animationNumber:{},
+			show: true
+		};
+	},
+	mounted(){
+		this.donghua();
+	},
+	watch:{
+		
+	},
+	methods: {
+		donghua() {
+			//进入
+			// #ifndef APP-NVUE
+			this.animationData = uni
+				.createAnimation({
+					duration: this.duration / 2,
+					timingFunction: 'ease'
+				})
+				.top(this.top - this.radius)
+				.opacity(0.9)
+				.step()
+				.export();
+			// #endif
+
+			// #ifdef APP-NVUE
+			if (!this.$refs['ani']) return;
+			animation.transition(
+				this.$refs['ani'].ref,
+				{
+					styles: {
+						transform: `translateY(-${this.radius/2}px)`,
+						opacity: 1
+					},
+					duration: this.duration/2,
+					timingFunction: 'linear',
+					needLayout: false,
+					delay: 0
+				}
+			);
+			// #endif
+
+			//停留
+			setTimeout(() => {
+				//消失
+				// #ifndef APP-NVUE
+				this.animationData = uni
+					.createAnimation({
+						duration: this.duration / 2,
+						timingFunction: 'ease'
+					})
+					.top(this.top - this.radius * 2)
+					.opacity(0)
+					.step()
+					.export();
+				// #endif
+				
+				// #ifdef APP-NVUE
+				if (!this.$refs['ani']) return;
+				animation.transition(
+					this.$refs['ani'].ref,
+					{
+						styles: {
+							transform: `translateY(-${this.radius}px)`,
+							opacity: 0
+						},
+						duration: this.duration/2,
+						timingFunction: 'linear',
+						needLayout: false,
+						delay: 0
+					}
+				);
+				// #endif
+			}, this.wait);
+		}
+	}
+};
+</script>
+
+<style scoped>
+.message {
+	position: fixed;
+	z-index: 99999;
+	opacity: 9;
+}
+.round {
+	border-radius: 5000px;
+}
+.bg-gradual-orange {
+	/* #ifndef APP-NVUE */
+	background-image: linear-gradient(45deg, #222222, #333333);
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	background-image: linear-gradient(to bottom right, #222222, #333333);
+	/* #endif */
+	color: #ffffff;
+}
+.shadow {
+	box-shadow: 4px 4px 5px rgba(217, 109, 26, 0.2);
+}
+.flex {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+}
+.justify-start {
+	justify-content: flex-start;
+}
+.cu-avatar {
+	/* #ifndef APP-NVUE */
+	font-variant: small-caps;
+	display: inline-flex;
+	white-space: nowrap;
+	background-size: cover;
+	background-position: center;
+	vertical-align: middle;
+	/* #endif */
+	margin: 0;
+	padding: 0;
+	text-align: center;
+	justify-content: center;
+	align-items: center;
+	background-color: #ccc;
+	color: #ffffff;
+	position: relative;
+	width: 20px;
+	height: 20px;
+	font-size: 1.5em;
+}
+
+/* #ifdef APP-NVUE */
+.avatarimg {
+	width: 20px;
+	height: 20px;
+	border-radius: 50px;
+}
+/* #endif */
+
+.cu-a-sm {
+	width: 20px;
+	height: 20px;
+	font-size: 0.8em;
+}
+.padding-lr-sm {
+	padding-left: 20upx;
+	padding-right: 20upx;
+}
+.align-center {
+	align-items: center;
+}
+.margin-left-xs {
+	margin-left: 10upx;
+}
+.text-bold {
+	font-weight: bold;
+}
+.margin-lr-sm {
+	margin-left: 20upx;
+	margin-right: 20upx;
+}
+.text-sm {
+	font-size: 12px;
+	color: #ffffff;
+}
+</style>

File diff suppressed because it is too large
+ 7 - 0
components/common/tui-clipboard/clipboard.min.js


+ 53 - 0
components/common/tui-clipboard/tui-clipboard.js

@@ -0,0 +1,53 @@
+/**
+ * 复制文本 兼容H5
+ * 来自 ThorUI | 文档地址: www.donarui.com
+ * @author echo.
+ * @version 1.0.0
+ **/
+// #ifdef H5
+import ClipboardJS from "./clipboard.min.js"
+// #endif
+const thorui = {
+	/**
+	 * data 需要复制的数据
+	 * callback 回调
+	 * e 当用户点击后需要先请求接口再进行复制时,需要传入此参数,或者将异步请求转为同步 (H5端)
+	 * **/
+	getClipboardData: function(data,callback,e) {
+		// #ifdef APP-PLUS || MP
+		uni.setClipboardData({
+			data: data,
+			success(res) {
+				("function" == typeof callback) && callback(true)
+			},
+			fail(res) {
+				("function" == typeof callback) && callback(false)
+			}
+		})
+		// #endif
+
+		// #ifdef H5
+		let event = window.event || e || {}
+		let clipboard = new ClipboardJS("", {
+			text: () => data
+		})
+		clipboard.on('success', (e) => {
+			("function" == typeof callback) && callback(true)
+			clipboard.off('success')
+			clipboard.off('error')
+			clipboard.destroy()
+		});
+		clipboard.on('error', (e) => {
+			("function" == typeof callback) && callback(false)
+			clipboard.off('success')
+			clipboard.off('error')
+			clipboard.destroy()
+		});
+		clipboard.onClick(event)
+		// #endif
+	}
+};
+
+module.exports = {
+	getClipboardData: thorui.getClipboardData
+};

+ 286 - 0
components/common/tui-poster/tui-poster.js

@@ -0,0 +1,286 @@
+const poster = {
+	/**
+	 * 绘制分享海报 
+	 * @param canvasId 
+	 * @param winWidth 宽度
+	 * @param winHeight 高度
+	 * @param imgs 主图以及二维码图
+	 *  格式 {mainPic: mainPic,qrcode: qrcode}
+	 * @param text 描述文字
+	 * @param name 程序名称
+	 **/
+	drawPoster(canvasId, winWidth, winHeight, imgs, text, name, callback) {
+		//获取绘图上下文 context
+		let context = uni.createCanvasContext(canvasId)
+		// 海报背景
+		const grd = context.createLinearGradient(0, winWidth, winHeight, 0)
+		grd.addColorStop(0, '#FFF')
+		grd.addColorStop(1, '#FFF')
+		// Fill with gradient
+		context.setFillStyle(grd);
+		context.fillRect(0, 0, winWidth, winHeight);
+		// 主图
+		let width = winWidth,
+			height = winHeight - 300;
+
+		context.drawImage(imgs.mainPic, 0, 0, width, height)
+		let a = uni.upx2px(40) //定义行高
+		context.setFontSize(uni.upx2px(50))
+		context.setFillStyle("#000")
+		//名称很长调用方法将文字折行,传参 文字内容text,画布context
+		let w = poster.getTextWidth(text, context) || uni.upx2px(550) * 2
+		let x = winWidth / 15;
+		let row = poster.wrapText(text, Math.floor(w), context)
+		//console.log(w);
+		for (let i = 0; i < row.length; i++) {
+			context.fillText(row[i], (winWidth/ 4) + 80, (winHeight - uni.upx2px(360)) + a * i)
+		}
+		// 识别小程序二维码 
+		context.setFontSize(uni.upx2px(36))
+		let w1 = poster.getTextWidth(name, context) || 140
+		context.drawImage(imgs.qrcode, x, (winHeight - uni.upx2px(500)), winWidth/4, winWidth/4)
+		context.setFillStyle("#152338")
+		context.fillText('邀请您加入'+name,  (winWidth/4) + 80, (winHeight - uni.upx2px(300)))
+		context.setFillStyle("#333")
+		context.setFontSize(uni.upx2px(24))
+		context.fillText('长按识别·立即体验', (winWidth/4) + 80, (winHeight - uni.upx2px(250)))
+		context.draw(false, () => {
+			poster.createPoster(canvasId, winWidth, winHeight, (res) => {
+				callback && callback(res)
+			})
+		})
+	},
+	/**
+	 *绘制商品海报 
+	 * @param canvasId 
+	 * @param winWidth 宽度
+	 * @param winHeight 高度
+	 * @param imgs 主图以及二维码图
+	 *  格式 {mainPic: mainPic,qrcode: qrcode}
+	 * @param text 商品名称
+	 * @param price 价格 格式 12.00,10.50
+	 * @param originalPrice 原价 格式 12.00,10.50
+	 * @param name 程序名称
+	 **/
+	drawGoodsPoster(canvasId, winWidth, winHeight, imgs, text, price, originalPrice, name, callback) {
+		let scaleRatio = 2
+		//获取绘图上下文 context
+		let context = uni.createCanvasContext(canvasId)
+		// 海报背景
+		const grd = context.createLinearGradient(0, winWidth, winHeight, 0)
+		grd.addColorStop(0, '#FFFFFF')
+		grd.addColorStop(1, '#FFFFFF')
+		// Fill with gradient
+		context.setFillStyle(grd);
+		context.fillRect(0, 0, winWidth, winHeight);
+		// 主图
+		let width = uni.upx2px(500 * scaleRatio),
+			height = uni.upx2px(500 * scaleRatio);
+		context.drawImage(imgs.mainPic, (winWidth - width) / 2, uni.upx2px(30 * scaleRatio), width, height)
+
+		let a = uni.upx2px(40 * scaleRatio) //定义行高
+		context.setFontSize(uni.upx2px(30 * scaleRatio))
+		context.setFillStyle("#343434")
+		let w = uni.upx2px(468 * scaleRatio)
+		//名称很长调用方法将文字折行,传参 文字内容text,画布context
+		let row = poster.wrapText(text, Math.floor(w), context, 2)
+		for (let i = 0; i < row.length; i++) {
+			context.fillText(row[i], uni.upx2px(30 * scaleRatio), uni.upx2px(580 * scaleRatio) + a * i)
+		}
+		context.setFillStyle("#EB0909")
+		context.setFontSize(uni.upx2px(26 * scaleRatio))
+		context.fillText('¥', uni.upx2px(30 * scaleRatio), uni.upx2px(680 * scaleRatio))
+		context.setFontSize(uni.upx2px(36 * scaleRatio))
+		let priceArr = Number(price).toFixed(2).toString().split('.')
+		context.fillText(priceArr[0], uni.upx2px(56 * scaleRatio), uni.upx2px(680 * scaleRatio))
+		let w1 = poster.getTextWidth(priceArr[0], context) || 35
+		context.setFontSize(uni.upx2px(26 * scaleRatio))
+		context.setFillStyle("#EB0909")
+		context.fillText(`.${priceArr[1]}`, uni.upx2px(60 * scaleRatio) + w1, uni.upx2px(680 * scaleRatio))
+		context.setFillStyle("#999999")
+		context.setFontSize(uni.upx2px(24 * scaleRatio))
+		
+		if(originalPrice>0){
+			let w2 = uni.upx2px(76 * scaleRatio) + w1 + (poster.getTextWidth(`.${priceArr[1]}`, context) || 32)
+			context.fillText(`¥${originalPrice}`, w2, uni.upx2px(680 * scaleRatio))
+			context.moveTo(w2, uni.upx2px(672 * scaleRatio))
+			context.lineTo((w2 + 50 * scaleRatio), uni.upx2px(672 * scaleRatio))
+			context.setStrokeStyle('#999999')
+			context.stroke(); //对当前路径进行描边
+		}
+		
+		// 识别小程序二维码 
+		let x = winWidth - uni.upx2px(46 + 130) * scaleRatio;
+		context.drawImage(imgs.qrcode, x, uni.upx2px(735 * scaleRatio), uni.upx2px(130 * scaleRatio), uni.upx2px(130 *
+			scaleRatio))
+		context.setFillStyle("#333")
+		context.setFontSize(uni.upx2px(32 * scaleRatio))
+		context.fillText(name, uni.upx2px(40 * scaleRatio), uni.upx2px(780 * scaleRatio))
+		context.setFontSize(uni.upx2px(24 * scaleRatio))
+		context.fillText('长按识别·立即体验', uni.upx2px(40 * scaleRatio), uni.upx2px(835 * scaleRatio))
+		context.draw(false, () => {
+			poster.createPoster(canvasId, winWidth, winHeight, (res) => {
+				callback && callback(res)
+			})
+		})
+	},
+	//生成海报图片(分享所需图片)
+	createPoster(canvasId, winWidth, winHeight, callback) {
+		var that = this;
+		// let scaleRatio= uni.getSystemInfoSync().scaleRatio
+		uni.canvasToTempFilePath({
+			x: 0,
+			y: 0,
+			// width: winWidth * scaleRatio,
+			// height: Math.round(winHeight * scaleRatio),
+			// destWidth: winWidth * scaleRatio,
+			// destHeight: Math.round(winHeight) * scaleRatio,
+			canvasId: canvasId,
+			fileType: 'png',
+			quality: 1,
+			success: function(res) {
+				callback && callback(res.tempFilePath)
+			},
+			fail() {
+				callback && callback(false)
+			}
+		})
+	},
+	// 将海报图片保存到本地
+	saveImage(file) {
+		uni.saveImageToPhotosAlbum({
+			filePath: file,
+			success(res) {
+				uni.showToast({
+					title: '图片保存成功'
+				})
+			}
+		})
+	},
+	//获取文本宽度(请先查看支持平台,App 端 2.8.12+ 支持)
+	getTextWidth(text, context) {
+		const metrics = context.measureText(text)
+		return metrics.width || 0
+	},
+	//图片转成本地文件[同步执行]
+	async getImage(url) {
+		return await new Promise((resolve, reject) => {
+			uni.downloadFile({
+				url: url,
+				success: res => {
+					resolve(res.tempFilePath);
+				},
+				fail: res => {
+					reject(false)
+				}
+			})
+		})
+	},
+	//canvas多文字换行
+	wrapText(text, width, context, rows = 2) {
+		let txtArr = text.split('')
+		let temp = ''
+		let row = []
+		for (let i = 0, len = txtArr.length; i < len; i++) {
+			if (context.measureText(temp).width < width) {
+				temp += txtArr[i]
+			} else {
+				i--
+				row.push(temp)
+				temp = ''
+			}
+		}
+		row.push(temp)
+		if (row.length > rows) {
+			let rowCut = row.slice(0, rows);
+			let rowPart = rowCut[rows - 1];
+			let test = "";
+			let empty = [];
+			for (let j = 0, length = rowPart.length; j < length; j++) {
+				if (context.measureText(test).width < width - 20) {
+					test += rowPart[j];
+				} else {
+					break;
+				}
+			}
+			empty.push(test);
+			let group = empty[0] + "...";
+			rowCut.splice(rows - 1, 1, group);
+			row = rowCut;
+		}
+		return row
+	},
+	//删除已缓存文件,防止超出存储空间大小限制[同步执行]
+	async removeSavedFile() {
+		//使用前请先查看支持平台(其他方案:也可以先渲染出图片,当图片加载完成时执行相关方法)
+		let count = 0;
+		return await new Promise((resolve, reject) => {
+			uni.getSavedFileList({
+				success(res) {
+					console.log(res)
+					count = res.fileList.length;
+					if (count > 0) {
+						let num = 0;
+						let list = res.fileList || []
+						list.forEach(item => {
+							console.log("执行删除文件。。。")
+							uni.removeSavedFile({
+								filePath: item.filePath,
+								complete(res) {
+									num++;
+									if (num === count) {
+										resolve(true)
+									}
+								}
+							})
+						})
+					} else {
+						resolve(true)
+					}
+				},
+				fail() {
+					reject(false)
+					console.log("执行删除文件失败")
+				}
+			})
+		})
+	},
+	//当服务器端返回图片base64时,转成本地文件[同步执行]
+	async getImagebyBase64(base64) {
+		//使用前先查看支持平台
+		// #ifdef MP-WEIXIN
+		return await new Promise((resolve, reject) => {
+			const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
+			let arrayBuffer = wx.base64ToArrayBuffer(bodyData)
+			//getuuid:注意这里名称需要动态生成(名称相同部分机型会出现写入失败,显示的是上次生成的图片)
+			const filePath = `${wx.env.USER_DATA_PATH}/${poster.getuuid()}.${format}`;
+			wx.getFileSystemManager().writeFile({
+				filePath,
+				data: arrayBuffer,
+				encoding: 'binary',
+				success() {
+					resolve(filePath);
+				},
+				fail() {
+					reject(false)
+				}
+			})
+		})
+		// #endif
+	},
+	getuuid() {
+		let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+			return (c === 'x' ? (Math.random() * 16) | 0 : 'r&0x3' | '0x8').toString(16);
+		});
+		return uuid;
+	}
+}
+
+module.exports = {
+	drawPoster: poster.drawPoster,
+	drawGoodsPoster: poster.drawGoodsPoster,
+	getImage: poster.getImage,
+	removeSavedFile: poster.removeSavedFile,
+	getImagebyBase64: poster.getImagebyBase64,
+	saveImage: poster.saveImage
+};

+ 268 - 0
components/common/tui-validation/tui-validation.js

@@ -0,0 +1,268 @@
+/**
+ * 表单验证
+ * @author echo.
+ * @version 1.5.0
+ **/
+
+const form = {
+	//非必填情况下,如果值为空,则不进行校验
+	//当出现错误时返回错误消息,否则返回空即为验证通过
+	/*
+	 formData:Object 表单对象。{key:value,key:value},key==rules.name
+	 rules: Array [{name:name,rule:[],msg:[]},{name:name,rule:[],msg:[]}]
+			name:name 属性=> 元素的名称
+			rule:字符串数组 ["required","isMobile","isEmail","isCarNo","isIdCard","isAmount","isNum","isChinese","isEnglish",isEnAndNo","isSpecial","isEmoji",""isDate","isUrl","isSame:key","range:[1,9]","minLength:9","maxLength:Number"]
+			msg:数组 []。 与数组 rule 长度相同,对应的错误提示信息
+	*/
+	validation: function(formData, rules) {
+		for (let item of rules) {
+			let key = item.name;
+			let rule = item.rule;
+			let msgArr = item.msg;
+			if (!key || !rule || rule.length === 0 || !msgArr || msgArr.length === 0) {
+				continue;
+			}
+			for (let i = 0, length = rule.length; i < length; i++) {
+				let ruleItem = rule[i];
+				let msg = msgArr[i];
+				if (!ruleItem || !msg || (!~rule.indexOf("required") && formData[key].toString().length === 0)) {
+					continue;
+				}
+				//数据处理
+				let value = null;
+				if (~ruleItem.indexOf(":")) {
+					let temp = ruleItem.split(":");
+					ruleItem = temp[0];
+					value = temp[1];
+				}
+				let isError = false;
+				switch (ruleItem) {
+					case "required":
+						isError = form._isNullOrEmpty(formData[key]);
+						break;
+					case "isMobile":
+						isError = !form._isMobile(formData[key]);
+						break;
+					case "isEmail":
+						isError = !form._isEmail(formData[key]);
+						break;
+					case "isCarNo":
+						isError = !form._isCarNo(formData[key]);
+						break;
+					case "isIdCard":
+						isError = !form._isIdCard(formData[key]);
+						break;
+					case "isAmount":
+						isError = !form._isAmount(formData[key]);
+						break;
+					case "isNum":
+						isError = !form._isNum(formData[key]);
+						break;
+					case "isChinese":
+						isError = !form._isChinese(formData[key]);
+						break;
+					case "isEnglish":
+						isError = !form._isEnglish(formData[key]);
+						break;
+					case "isEnAndNo":
+						isError = !form._isEnAndNo(formData[key]);
+						break;
+					case "isEnOrNo":
+						isError = !form._isEnOrNo(formData[key]);
+						break;
+					case "isSpecial":
+						isError = form._isSpecial(formData[key]);
+						break;
+					case "isEmoji":
+						isError = form._isEmoji(formData[key]);
+						break;
+					case "isDate":
+						isError = !form._isDate(formData[key]);
+						break;
+					case "isUrl":
+						isError = !form._isUrl(formData[key]);
+						break;
+					case "isSame":
+						isError = !form._isSame(formData[key], formData[value]);
+						break;
+					case "range":
+						let range = null;
+						try {
+							range = JSON.parse(value);
+							if (range.length <= 1) {
+								throw new Error("range值传入有误!")
+							}
+						} catch (e) {
+							return "range值传入有误!"
+						}
+						isError = !form._isRange(formData[key], range[0], range[1])
+						break;
+					case "minLength":
+						isError = !form._minLength(formData[key], value)
+						break;
+					case "maxLength":
+						isError = !form._maxLength(formData[key], value)
+						break;
+					default:
+						break;
+				}
+				if (isError) {
+					return msg;
+				}
+			}
+		}
+		return "";
+	},
+	_isNullOrEmpty: function(value) {
+		return (value === null || value === '' || value === undefined) ? true : false;
+	},
+	_isMobile: function(value) {
+		return /^(?:13\d|14\d|15\d|16\d|17\d|18\d|19\d)\d{5}(\d{3}|\*{3})$/.test(value);
+	},
+	_isEmail: function(value) {
+		return /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/.test(value);
+	},
+	_isCarNo: function(value) {
+		// 新能源车牌
+		const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
+		// 旧车牌
+		const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
+		if (value.length === 7) {
+			return creg.test(value);
+		} else if (value.length === 8) {
+			return xreg.test(value);
+		} else {
+			return false;
+		}
+	},
+	_isIdCard: function(value) {
+		let idCard = value;
+		if (idCard.length == 15) {
+			return this.__isValidityBrithBy15IdCard;
+		} else if (idCard.length == 18) {
+			let arrIdCard = idCard.split("");
+			if (this.__isValidityBrithBy18IdCard(idCard) && this.__isTrueValidateCodeBy18IdCard(arrIdCard)) {
+				return true;
+			} else {
+				return false;
+			}
+		} else {
+			return false;
+		}
+	},
+	__isTrueValidateCodeBy18IdCard: function(arrIdCard) {
+		let sum = 0;
+		let Wi = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2, 1];
+		let ValideCode = [1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2];
+		if (arrIdCard[17].toLowerCase() == 'x') {
+			arrIdCard[17] = 10;
+		}
+		for (let i = 0; i < 17; i++) {
+			sum += Wi[i] * arrIdCard[i];
+		}
+		let valCodePosition = sum % 11;
+		if (arrIdCard[17] == ValideCode[valCodePosition]) {
+			return true;
+		} else {
+			return false;
+		}
+	},
+	__isValidityBrithBy18IdCard: function(idCard18) {
+		let year = idCard18.substring(6, 10);
+		let month = idCard18.substring(10, 12);
+		let day = idCard18.substring(12, 14);
+		let temp_date = new Date(year, parseFloat(month) - 1, parseFloat(day));
+		if (temp_date.getFullYear() != parseFloat(year) || temp_date.getMonth() != parseFloat(month) - 1 || temp_date.getDate() !=
+			parseFloat(day)) {
+			return false;
+		} else {
+			return true;
+		}
+	},
+	__isValidityBrithBy15IdCard: function(idCard15) {
+		let year = idCard15.substring(6, 8);
+		let month = idCard15.substring(8, 10);
+		let day = idCard15.substring(10, 12);
+		let temp_date = new Date(year, parseFloat(month) - 1, parseFloat(day));
+
+		if (temp_date.getYear() != parseFloat(year) || temp_date.getMonth() != parseFloat(month) - 1 || temp_date.getDate() !=
+			parseFloat(day)) {
+			return false;
+		} else {
+			return true;
+		}
+	},
+	_isAmount: function(value) {
+		//金额,只允许保留两位小数
+		return /^([0-9]*[.]?[0-9])[0-9]{0,1}$/.test(value);
+	},
+	_isNum: function(value) {
+		//只能为数字
+		return /^[0-9]+$/.test(value);
+	},
+	_isChinese: function(value) {
+		let reg = /.*[\u4e00-\u9fa5]+.*$/;
+		return value !== "" && reg.test(value) && !form._isSpecial(value) && !form._isEmoji(value)
+	},
+	_isEnglish: function(value) {
+		return /^[a-zA-Z]*$/.test(value)
+	},
+	_isEnAndNo: function(value) {
+		//8~20位数字和字母组合
+		return /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,20}$/.test(value);
+	},
+	_isEnOrNo: function(value) {
+		//英文或者数字
+		let reg = /.*[\u4e00-\u9fa5]+.*$/;
+		let result = true;
+		if (reg.test(value) || form._isSpecial(value) || form._isEmoji(value)) {
+			result = false
+		}
+		return result
+	},
+	_isSpecial: function(value) {
+		//是否包含特殊字符
+		let regEn = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im,
+			regCn = /[·!#¥(——):;“”‘、,|《。》?、【】[\]]/im;
+		if (regEn.test(value) || regCn.test(value)) {
+			return true;
+		}
+		return false;
+	},
+	_isEmoji: function(value) {
+		//是否包含表情
+		return /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g.test(value);
+	},
+	_isDate: function(value) {
+		//2019-10-12
+		const reg =
+			/^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$/;
+		return reg.test(value);
+	},
+	_isUrl: function(value) {
+		return /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test(value);
+	},
+	_isSame: function(value1, value2) {
+		return value1 === value2
+	},
+	_isRange: function(value, range1, range2) {
+		if ((!range1 && range1 != 0) && (!range2 && range2 != 0)) {
+			return true;
+		} else if (!range1 && range1 != 0) {
+			return value <= range2
+		} else if (!range2 && range2 != 0) {
+			return value >= range1
+		} else {
+			return value >= range1 && value <= range2
+		}
+	},
+	_minLength: function(value, min) {
+		return value.length >= Number(min)
+	},
+	_maxLength: function(value, max) {
+		return value.length <= Number(max)
+	}
+};
+module.exports = {
+	validation: form.validation
+};

File diff suppressed because it is too large
+ 0 - 0
components/common/tui-validation/tui-validation.min.js


+ 814 - 0
components/jyf-parser/jyf-parser.vue

@@ -0,0 +1,814 @@
+<!--
+  parser 主模块组件
+  github:https://github.com/jin-yufeng/Parser 
+  docs:https://jin-yufeng.github.io/Parser
+  插件市场:https://ext.dcloud.net.cn/plugin?id=805
+  author:JinYufeng
+  update:2020/04/14
+-->
+<template>
+	<view>
+		<slot v-if="!nodes.length" />
+		<!--#ifdef APP-PLUS-NVUE-->
+		<web-view id="top" ref="web" :src="src" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
+		<!--#endif-->
+		<!--#ifndef APP-PLUS-NVUE-->
+		<view id="top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')" :animation="scaleAm" @tap="_tap"
+		 @touchstart="_touchstart" @touchmove="_touchmove">
+			<!--#ifdef H5-->
+			<div :id="'rtf'+uid"></div>
+			<!--#endif-->
+			<!--#ifndef H5-->
+			<trees :nodes="nodes" :lazy-load="lazyLoad" :loadVideo="loadVideo" />
+			<image v-for="(item, index) in imgs" v-bind:key="index" :id="index" :src="item" hidden @load="_load" />
+			<!--#endif-->
+		</view>
+		<!--#endif-->
+	</view>
+</template>
+
+<script>
+	// #ifndef H5 || APP-PLUS-NVUE
+	import trees from './libs/trees';
+	var cache = {},
+		// #ifdef MP-WEIXIN || MP-TOUTIAO
+		fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
+		// #endif
+		Parser = require('./libs/MpHtmlParser.js');
+	var document; // document 补丁包 https://jin-yufeng.github.io/Parser/#/instructions?id=document
+	// 计算 cache 的 key
+	function hash(str) {
+		for (var i = str.length, val = 5381; i--;)
+			val += (val << 5) + str.charCodeAt(i);
+		return val;
+	}
+	// #endif
+	// #ifdef H5 || APP-PLUS-NVUE
+	var rpx = uni.getSystemInfoSync().screenWidth / 750,
+		cfg = require('./libs/config.js');
+	// #endif
+	// #ifdef APP-PLUS-NVUE
+	var dom = weex.requireModule('dom');
+	// #endif
+	export default {
+		name: 'parser',
+		data() {
+			return {
+				// #ifdef APP-PLUS
+				loadVideo: false,
+				// #endif
+				// #ifdef H5
+				uid: this._uid,
+				// #endif
+				// #ifdef APP-PLUS-NVUE
+				src: '',
+				height: 1,
+				// #endif
+				// #ifndef APP-PLUS-NVUE
+				scaleAm: '',
+				showAm: '',
+				imgs: [],
+				// #endif
+				nodes: []
+			}
+		},
+		// #ifndef H5 || APP-PLUS-NVUE
+		components: {
+			trees
+		},
+		// #endif
+		props: {
+			'html': null,
+			// #ifndef MP-ALIPAY
+			'autopause': {
+				type: Boolean,
+				default: true
+			},
+			// #endif
+			'autosetTitle': {
+				type: Boolean,
+				default: true
+			},
+			// #ifndef H5 || APP-PLUS-NVUE
+			'compress': Number,
+			'useCache': Boolean,
+			'xml': Boolean,
+			// #endif
+			'domain': String,
+			// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
+			'gestureZoom': Boolean,
+			// #endif
+			// #ifdef MP-WEIXIN || MP-QQ || H5 || APP-PLUS
+			'lazyLoad': Boolean,
+			// #endif
+			'selectable': Boolean,
+			'tagStyle': Object,
+			'showWithAnimation': Boolean,
+			'useAnchor': Boolean
+		},
+		watch: {
+			html(html) {
+				this.setContent(html);
+			}
+		},
+		mounted() {
+			// 图片数组
+			this.imgList = [];
+			this.imgList.each = function(f) {
+				for (var i = 0, len = this.length; i < len; i++)
+					this.setItem(i, f(this[i], i, this));
+			}
+			this.imgList.setItem = function(i, src) {
+				if (i == void 0 || !src) return;
+				// #ifndef MP-ALIPAY || APP-PLUS
+				// 去重
+				if (src.indexOf('http') == 0 && this.includes(src)) {
+					var newSrc = '';
+					for (var j = 0, c; c = src[j]; j++) {
+						if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
+						newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
+					}
+					newSrc += src.substr(j);
+					return this[i] = newSrc;
+				}
+				// #endif
+				this[i] = src;
+				// 暂存 data src
+				if (src.includes('data:image')) {
+					var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
+					if (!info) return;
+					// #ifdef MP-WEIXIN || MP-TOUTIAO
+					filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
+					fs && fs.writeFile({
+						filePath,
+						data: info[3],
+						encoding: info[2],
+						success: () => this[i] = filePath
+					})
+					// #endif
+					// #ifdef APP-PLUS
+					filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
+					var bitmap = new plus.nativeObj.Bitmap();
+					bitmap.loadBase64Data(src, () => {
+						bitmap.save(filePath, {}, () => {
+							bitmap.clear()
+							this[i] = filePath;
+						})
+					})
+					// #endif
+				}
+			}
+			if (this.html) this.setContent(this.html);
+		},
+		beforeDestroy() {
+			// #ifdef H5
+			if (this._observer) this._observer.disconnect();
+			// #endif
+			this.imgList.each(src => {
+				// #ifdef APP-PLUS
+				if (src && src.includes('_doc')) {
+					plus.io.resolveLocalFileSystemURL(src, entry => {
+						entry.remove();
+					});
+				}
+				// #endif
+				// #ifdef MP-WEIXIN || MP-TOUTIAO
+				if (src && src.includes(uni.env.USER_DATA_PATH))
+					fs && fs.unlink({
+						filePath: src
+					})
+				// #endif
+			})
+			clearInterval(this._timer);
+		},
+		methods: {
+			// #ifdef H5 || APP-PLUS-NVUE
+			_Dom2Str(nodes) {
+				var str = '';
+				for (var node of nodes) {
+					if (node.type == 'text')
+						str += node.text;
+					else {
+						str += ('<' + node.name);
+						for (var attr in node.attrs || {})
+							str += (' ' + attr + '="' + node.attrs[attr] + '"');
+						if (!node.children || !node.children.length) str += '>';
+						else str += ('>' + this._Dom2Str(node.children) + '</' + node.name + '>');
+					}
+				}
+				return str;
+			},
+			_handleHtml(html, append) {
+				if (typeof html != 'string') html = this._Dom2Str(html.nodes || html);
+				// 处理 rpx
+				if (html.includes('rpx'))
+					html = html.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * rpx + 'px');
+				if (!append) {
+					// 处理 tag-style 和 userAgentStyles
+					var style = '<style>@keyframes show{0%{opacity:0}100%{opacity:1}}';
+					for (var item in cfg.userAgentStyles)
+						style += `${item}{${cfg.userAgentStyles[item]}}`;
+					for (item in this.tagStyle)
+						style += `${item}{${this.tagStyle[item]}}`;
+					style += '</style>';
+					html = style + html;
+				}
+				return html;
+			},
+			// #endif
+			setContent(html, append) {
+				// #ifdef APP-PLUS-NVUE
+				if (!html) {
+					this.src = '';
+					this.height = 1;
+					return;
+				}
+				if (append) return;
+				plus.io.resolveLocalFileSystemURL('_doc', entry => {
+					entry.getDirectory('parser_tmp', {
+						create: true
+					}, entry => {
+						var fileName = Date.now() + '.html';
+						entry.getFile(fileName, {
+							create: true
+						}, entry => {
+							entry.createWriter(writer => {
+								writer.onwriteend = () => {
+									this.nodes = [1];
+									this.src = '_doc/parser_tmp/' + fileName;
+									this.$nextTick(function() {
+										entry.remove();
+									})
+								}
+								html =
+									'<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1' +
+									(this.selectable ? '' : ',user-scalable=no') +
+									'"><script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></' +
+									'script><base href="' + this.domain + '">' + this._handleHtml(html) +
+									'<script>"use strict";function post(t){uni.postMessage({data:t})}' +
+									(this.showWithAnimation ? 'document.body.style.animation="show .5s",' : '') +
+									'document.addEventListener("UniAppJSBridgeReady",function(){post({action:"load",text:document.body.innerText});var t=document.getElementsByTagName("title");t.length&&post({action:"getTitle",title:t[0].innerText});for(var e,o=document.getElementsByTagName("img"),n=[],i=0,r=0;e=o[i];i++)e.onerror=function(){post({action:"error",source:"img",target:this})},e.hasAttribute("ignore")||"A"==e.parentElement.nodeName||(e.i=r++,n.push(e.src),e.onclick=function(){post({action:"preview",img:{i:this.i,src:this.src}})});post({action:"getImgList",imgList:n});for(var a,s=document.getElementsByTagName("a"),c=0;a=s[c];c++)a.onclick=function(){var t,e=this.getAttribute("href");if("#"==e[0]){var r=document.getElementById(e.substr(1));r&&(t=r.offsetTop)}return post({action:"linkpress",href:e,offset:t}),!1};;for(var u,m=document.getElementsByTagName("video"),d=0;u=m[d];d++)u.style.maxWidth="100%",u.onerror=function(){post({action:"error",source:"video",target:this})}' +
+									(this.autopause ? ',u.onplay=function(){for(var t,e=0;t=m[e];e++)t!=this&&t.pause()}' : '') +
+									';for(var g,l=document.getElementsByTagName("audio"),p=0;g=l[p];p++)g.onerror=function(){post({action:"error",source:"audio",target:this})};window.onload=function(){post({action:"ready",height:document.body.scrollHeight})}});</' +
+									'script>';
+								writer.write(html);
+							});
+						})
+					})
+				})
+				// #endif
+				// #ifdef H5
+				if (!html) {
+					if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
+					return;
+				}
+				var div = document.createElement('div');
+				if (!append) {
+					if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
+					this.rtf = div;
+				} else {
+					if (!this.rtf) this.rtf = div;
+					else this.rtf.appendChild(div);
+				}
+				div.innerHTML = this._handleHtml(html, append);
+				for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
+					style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
+					style.setAttribute('scoped', 'true');
+				}
+				// 懒加载
+				if (!this._observer && this.lazyLoad && IntersectionObserver) {
+					this._observer = new IntersectionObserver(changes => {
+						for (let item, i = 0; item = changes[i++];) {
+							if (item.isIntersecting) {
+								item.target.src = item.target.getAttribute('data-src');
+								item.target.removeAttribute('data-src');
+								this._observer.unobserve(item.target);
+							}
+						}
+					}, {
+						rootMargin: '900px 0px 900px 0px'
+					})
+				}
+				var _ts = this;
+				// 获取标题
+				var title = this.rtf.getElementsByTagName('title');
+				if (title.length && this.autosetTitle)
+					uni.setNavigationBarTitle({
+						title: title[0].innerText
+					})
+				// 图片处理
+				this.imgList.length = 0;
+				var imgs = this.rtf.getElementsByTagName('img');
+				for (let i = 0, j = 0, img; img = imgs[i]; i++) {
+					img.style.maxWidth = '100%';
+					var src = img.getAttribute('src');
+					if (this.domain && src) {
+						if (src[0] == '/') {
+							if (src[1] == '/')
+								img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
+							else img.src = this.domain + src;
+						} else if (!src.includes('://')) img.src = this.domain + '/' + src;
+					}
+					if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
+						img.i = j++;
+						_ts.imgList.push(img.src || img.getAttribute('data-src'));
+						img.onclick = function() {
+							var preview = true;
+							this.ignore = () => preview = false;
+							_ts.$emit('imgtap', this);
+							if (preview) {
+								uni.previewImage({
+									current: this.i,
+									urls: _ts.imgList
+								});
+							}
+						}
+					}
+					img.onerror = function() {
+						_ts.$emit('error', {
+							source: 'img',
+							target: this
+						});
+					}
+					if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
+						img.setAttribute('data-src', img.src);
+						img.removeAttribute('src');
+						this._observer.observe(img);
+					}
+				}
+				// 链接处理
+				var links = this.rtf.getElementsByTagName('a');
+				for (var link of links) {
+					link.onclick = function() {
+						var jump = true,
+							href = this.getAttribute('href');
+						_ts.$emit('linkpress', {
+							href,
+							ignore: () => jump = false
+						});
+						if (jump && href) {
+							if (href[0] == '#') {
+								if (_ts.useAnchor) {
+									_ts.navigateTo({
+										id: href.substr(1)
+									})
+								}
+							} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
+								return true;
+							else {
+								uni.navigateTo({
+									url: href
+								})
+							}
+						}
+						return false;
+					}
+				}
+				// 视频处理
+				var videos = this.rtf.getElementsByTagName('video');
+				_ts.videoContexts = videos;
+				for (let video, i = 0; video = videos[i++];) {
+					video.style.maxWidth = '100%';
+					video.onerror = function() {
+						_ts.$emit('error', {
+							source: 'video',
+							target: this
+						});
+					}
+					video.onplay = function() {
+						if (_ts.autopause)
+							for (let item, i = 0; item = _ts.videoContexts[i++];)
+								if (item != this) item.pause();
+					}
+				}
+				// 音频处理
+				var audios = this.rtf.getElementsByTagName('audios');
+				for (var audio of audios)
+					audio.onerror = function() {
+						_ts.$emit('error', {
+							source: 'audio',
+							target: this
+						});
+					}
+				this.document = this.rtf;
+				if (!append) document.getElementById('rtf' + this._uid).appendChild(this.rtf);
+				this.$nextTick(() => {
+					this.nodes = [1];
+					this.$emit('load');
+				})
+				setTimeout(() => this.showAm = '', 500);
+				// #endif
+				// #ifndef H5 || APP-PLUS-NVUE
+				var nodes;
+				if (!html)
+					return this.nodes = [];
+				else if (typeof html == 'string') {
+					let parser = new Parser(html, this);
+					// 缓存读取
+					if (this.useCache) {
+						var hashVal = hash(html);
+						if (cache[hashVal])
+							nodes = cache[hashVal];
+						else {
+							nodes = parser.parse();
+							cache[hashVal] = nodes;
+						}
+					} else nodes = parser.parse();
+					this.$emit('parse', nodes);
+				} else if (Object.prototype.toString.call(html) == '[object Array]') {
+					// 非本插件产生的 array 需要进行一些转换
+					if (html.length && html[0].PoweredBy != 'Parser') {
+						let parser = new Parser(html, this);
+						(function f(ns) {
+							for (var i = 0, n; n = ns[i]; i++) {
+								if (n.type == 'text') continue;
+								n.attrs = n.attrs || {};
+								for (var item in n.attrs)
+									if (typeof n.attrs[item] != 'string') n.attrs[item] = n.attrs[item].toString();
+								parser.matchAttr(n, parser);
+								if (n.children && n.children.length) {
+									parser.STACK.push(n);
+									f(n.children);
+									parser.popNode(parser.STACK.pop());
+								} else n.children = void 0;
+							}
+						})(html);
+					}
+					nodes = html;
+				} else if (typeof html == 'object' && html.nodes) {
+					nodes = html.nodes;
+					console.warn('错误的 html 类型:object 类型已废弃');
+				} else
+					return console.warn('错误的 html 类型:' + typeof html);
+				// #ifdef APP-PLUS
+				this.loadVideo = false;
+				// #endif
+				if (document) this.document = new document(this.nodes, 'nodes', this);
+				if (append) this.nodes = this.nodes.concat(nodes);
+				else this.nodes = nodes;
+				if (nodes.length && nodes[0].title && this.autosetTitle)
+					uni.setNavigationBarTitle({
+						title: nodes[0].title
+					})
+				this.$nextTick(() => {
+					this.imgList.length = 0;
+					this.videoContexts = [];
+					// #ifdef MP-TOUTIAO
+					setTimeout(() => {
+						// #endif
+						var f = (cs) => {
+							for (let i = 0, c; c = cs[i++];) {
+								if (c.$options.name == 'trees') {
+									for (var j = c.nodes.length, item; item = c.nodes[--j];) {
+										if (item.c) continue;
+										if (item.name == 'img') {
+											this.imgList.setItem(item.attrs.i, item.attrs.src);
+											// #ifndef MP-ALIPAY
+											if (!c.observer && !c.imgLoad && item.attrs.i != '0') {
+												if (this.lazyLoad && uni.createIntersectionObserver) {
+													c.observer = uni.createIntersectionObserver(c);
+													c.observer.relativeToViewport({
+														top: 900,
+														bottom: 900
+													}).observe('._img', () => {
+														c.imgLoad = true;
+														c.observer.disconnect();
+													})
+												} else
+													c.imgLoad = true;
+											}
+											// #endif
+										}
+										// #ifndef MP-ALIPAY
+										else if (item.name == 'video') {
+											var ctx = uni.createVideoContext(item.attrs.id, c);
+											ctx.id = item.attrs.id;
+											this.videoContexts.push(ctx);
+										}
+										// #endif
+										// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
+										if (item.attrs && item.attrs.id) {
+											this.anchors = this.anchors || [];
+											this.anchors.push({
+												id: item.attrs.id,
+												node: c
+											})
+										}
+										// #endif
+									}
+								}
+								if (c.$children.length)
+									f(c.$children)
+							}
+						}
+						f(this.$children);
+						// #ifdef MP-TOUTIAO
+					}, 200)
+					this.$emit('load');
+					// #endif
+					// #ifdef APP-PLUS
+					setTimeout(() => {
+						this.loadVideo = true;
+					}, 3000);
+					// #endif
+				})
+				// #endif
+				// #ifndef APP-PLUS-NVUE
+				var height;
+				clearInterval(this._timer);
+				this._timer = setInterval(() => {
+					// #ifdef H5
+					var res = [this.rtf.getBoundingClientRect()];
+					// #endif
+					// #ifndef H5
+					// #ifdef APP-PLUS
+					uni.createSelectorQuery().in(this)
+					// #endif
+					// #ifndef APP-PLUS
+					this.createSelectorQuery()
+						// #endif
+						.select('#top').boundingClientRect().exec(res => {
+							// #endif
+							this.width = res[0].width;
+							if (res[0].height == height) {
+								this.$emit('ready', res[0])
+								clearInterval(this._timer);
+							}
+							height = res[0].height;
+							// #ifndef H5
+						});
+					// #endif
+				}, 350)
+				if (this.showWithAnimation && !append) this.showAm = 'animation:show .5s';
+				// #endif
+			},
+			getText(ns = this.nodes) {
+				// #ifdef APP-PLUS-NVUE
+				return this._text;
+				// #endif
+				// #ifdef H5
+				return this.rtf.innerText;
+				// #endif
+				// #ifndef H5 || APP-PLUS-NVUE
+				var txt = '';
+				for (var i = 0, n; n = ns[i++];) {
+					if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
+						.replace(/&amp;/g, '&');
+					else if (n.type == 'br') txt += '\n';
+					else {
+						// 块级标签前后加换行
+						var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
+							'0' && n.name[1] < '7');
+						if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
+						if (n.children) txt += this.getText(n.children);
+						if (block && txt[txt.length - 1] != '\n') txt += '\n';
+						else if (n.name == 'td' || n.name == 'th') txt += '\t';
+					}
+				}
+				return txt;
+				// #endif
+			},
+			navigateTo(obj) {
+				if (!this.useAnchor)
+					return obj.fail && obj.fail({
+						errMsg: 'Anchor is disabled'
+					})
+				// #ifdef APP-PLUS-NVUE
+				if (!obj.id)
+					dom.scrollToElement(this.$refs.web);
+				else
+					this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
+						'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop})');
+				return obj.success && obj.success({
+					errMsg: 'pageScrollTo:ok'
+				});
+				// #endif
+				// #ifdef H5
+				if (!obj.id) {
+					window.scrollTo(0, this.rtf.offsetTop);
+					return obj.success && obj.success({
+						errMsg: 'pageScrollTo:ok'
+					});
+				}
+				var target = document.getElementById(obj.id);
+				if (!target) return obj.fail && obj.fail({
+					errMsg: 'Label not found'
+				});
+				obj.scrollTop = this.rtf.offsetTop + target.offsetTop;
+				uni.pageScrollTo(obj);
+				// #endif
+				// #ifndef H5
+				var Scroll = (selector, component) => {
+					uni.createSelectorQuery().in(component ? component : this).select(selector).boundingClientRect().selectViewport()
+						.scrollOffset()
+						.exec(res => {
+							if (!res || !res[0])
+								return obj.fail && obj.fail({
+									errMsg: 'Label not found'
+								});
+							obj.scrollTop = res[1].scrollTop + res[0].top;
+							uni.pageScrollTo(obj);
+						})
+				}
+				if (!obj.id) Scroll('#top');
+				else {
+					// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
+					Scroll('#top >>> #' + obj.id + ', #top >>> .' + obj.id);
+					// #endif
+					// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
+					for (var anchor of this.anchors)
+						if (anchor.id == obj.id)
+							Scroll('#' + obj.id + ', .' + obj.id, anchor.node);
+					// #endif
+				}
+				// #endif
+			},
+			getVideoContext(id) {
+				// #ifndef APP-PLUS-NVUE
+				if (!id) return this.videoContexts;
+				else
+					for (var i = this.videoContexts.length; i--;)
+						if (this.videoContexts[i].id == id) return this.videoContexts[i];
+				// #endif
+			},
+			// 预加载
+			preLoad(html, num) {
+				// #ifdef H5 || APP-PLUS-NVUE
+				if (html.constructor == Array)
+					html = this._Dom2Str(html);
+				var script = "var contain=document.createElement('div');contain.innerHTML='" + html.replace(/'/g, "\\'") +
+					"';for(var imgs=contain.querySelectorAll('img'),i=imgs.length-1;i>=" + num +
+					";i--)imgs[i].removeAttribute('src');";
+				// #endif
+				// #ifdef APP-PLUS-NVUE
+				this.$refs.web.evalJs(script);
+				// #endif
+				// #ifdef H5
+				eval(script);
+				// #endif
+				// #ifndef H5 || APP-PLUS-NVUE
+				if (typeof html == 'string') {
+					var id = hash(html);
+					html = new Parser(html, this).parse();
+					cache[id] = html;
+				}
+				var wait = [];
+				(function f(ns) {
+					for (var i = 0, n; n = ns[i++];) {
+						if (n.name == 'img' && n.attrs.src && !wait.includes(n.attrs.src))
+							wait.push(n.attrs.src);
+						f(n.children || []);
+					}
+				})(html);
+				if (num) wait = wait.slice(0, num);
+				this._wait = (this._wait || []).concat(wait);
+				if (!this.imgs) this.imgs = this._wait.splice(0, 15);
+				else if (this.imgs.length < 15)
+					this.imgs = this.imgs.concat(this._wait.splice(0, 15 - this.imgs.length));
+				// #endif
+			},
+			// #ifdef APP-PLUS-NVUE
+			_message(e) {
+				// 接收 web-view 消息
+				var data = e.detail.data[0];
+				if (data.action == 'load') {
+					this.$emit('load');
+					this._text = data.text;
+				} else if (data.action == 'getTitle') {
+					if (this.autosetTitle)
+						uni.setNavigationBarTitle({
+							title: data.title
+						})
+				} else if (data.action == 'getImgList') {
+					this.imgList.length = 0;
+					for (var i = data.imgList.length; i--;)
+						this.imgList.setItem(i, data.imgList[i]);
+				} else if (data.action == 'preview') {
+					var preview = true;
+					data.img.ignore = () => preview = false;
+					this.$emit('imgtap', data.img);
+					if (preview)
+						uni.previewImage({
+							current: data.img.i,
+							urls: this.imgList
+						})
+				} else if (data.action == 'linkpress') {
+					var jump = true,
+						href = data.href;
+					this.$emit('linkpress', {
+						href,
+						ignore: () => jump = false
+					})
+					if (jump && href) {
+						if (href[0] == '#') {
+							if (this.useAnchor)
+								dom.scrollToElement(this.$refs.web, {
+									offset: data.offset
+								})
+						} else if (href.includes('://'))
+							plus.runtime.openWeb(href);
+						else
+							uni.navigateTo({
+								url: href
+							})
+					}
+				} else if (data.action == 'error')
+					this.$emit('error', {
+						source: data.source,
+						target: data.target
+					})
+				else if (data.action == 'ready') {
+					this.height = data.height;
+					this.$nextTick(() => {
+						uni.createSelectorQuery().in(this).select('#top').boundingClientRect().exec(res => {
+							this.rect = res[0];
+							this.$emit('ready', res[0]);
+						})
+					})
+				}
+			},
+			// #endif
+			// #ifndef APP-PLUS-NVUE
+			// #ifndef H5
+			_load(e) {
+				if (this._wait.length)
+					this.$set(this.imgs, e.target.id, this._wait.shift());
+			},
+			// #endif
+			_tap(e) {
+				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
+				if (this.gestureZoom && e.timeStamp - this._lastT < 300) {
+					var initY = e.touches[0].pageY - e.currentTarget.offsetTop;
+					if (this._zoom) {
+						this._scaleAm.translateX(0).scale(1).step();
+						uni.pageScrollTo({
+							scrollTop: (initY + this._initY) / 2 - e.touches[0].clientY,
+							duration: 400
+						})
+					} else {
+						var initX = e.touches[0].pageX - e.currentTarget.offsetLeft;
+						this._initY = initY;
+						this._scaleAm = uni.createAnimation({
+							transformOrigin: `${initX}px ${this._initY}px 0`,
+							timingFunction: 'ease-in-out'
+						});
+						// #ifdef MP-TOUTIAO
+						this._scaleAm.opacity(1);
+						// #endif
+						this._scaleAm.scale(2).step();
+						this._tMax = initX / 2;
+						this._tMin = (initX - this.width) / 2;
+						this._tX = 0;
+					}
+					this._zoom = !this._zoom;
+					this.scaleAm = this._scaleAm.export();
+				}
+				this._lastT = e.timeStamp;
+				// #endif
+			},
+			_touchstart(e) {
+				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
+				if (e.touches.length == 1)
+					this._initX = this._lastX = e.touches[0].pageX;
+				// #endif
+			},
+			_touchmove(e) {
+				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
+				var diff = e.touches[0].pageX - this._lastX;
+				if (this._zoom && e.touches.length == 1 && Math.abs(diff) > 20) {
+					this._lastX = e.touches[0].pageX;
+					if ((this._tX <= this._tMin && diff < 0) || (this._tX >= this._tMax && diff > 0))
+						return;
+					this._tX += (diff * Math.abs(this._lastX - this._initX) * 0.05);
+					if (this._tX < this._tMin) this._tX = this._tMin;
+					if (this._tX > this._tMax) this._tX = this._tMax;
+					this._scaleAm.translateX(this._tX).step();
+					this.scaleAm = this._scaleAm.export();
+				}
+				// #endif
+			}
+			// #endif
+		}
+	}
+</script>
+
+<style>
+	@keyframes show {
+		0% {
+			opacity: 0
+		}
+
+		100% {
+			opacity: 1;
+		}
+	}
+
+	/* #ifdef MP-WEIXIN */
+	:host {
+		display: block;
+		overflow: scroll;
+		-webkit-overflow-scrolling: touch;
+	}
+
+	/* #endif */
+</style>

+ 102 - 0
components/jyf-parser/libs/CssHandler.js

@@ -0,0 +1,102 @@
+/*
+  解析和匹配 Css 的选择器
+  github:https://github.com/jin-yufeng/Parser
+  docs:https://jin-yufeng.github.io/Parser
+  author:JinYufeng
+  update:2020/03/15
+*/
+var cfg = require('./config.js');
+class CssHandler {
+	constructor(tagStyle) {
+		var styles = Object.assign({}, cfg.userAgentStyles);
+		for (var item in tagStyle)
+			styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
+		this.styles = styles;
+	}
+	getStyle = data => this.styles = new CssParser(data, this.styles).parse();
+	match(name, attrs) {
+		var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
+		if (attrs.class) {
+			var items = attrs.class.split(' ');
+			for (var i = 0, item; item = items[i]; i++)
+				if (tmp = this.styles['.' + item])
+					matched += tmp + ';';
+		}
+		if (tmp = this.styles['#' + attrs.id])
+			matched += tmp + ';';
+		return matched;
+	}
+}
+module.exports = CssHandler;
+class CssParser {
+	constructor(data, init) {
+		this.data = data;
+		this.floor = 0;
+		this.i = 0;
+		this.list = [];
+		this.res = init;
+		this.state = this.Space;
+	}
+	parse() {
+		for (var c; c = this.data[this.i]; this.i++)
+			this.state(c);
+		return this.res;
+	}
+	section = () => this.data.substring(this.start, this.i);
+	isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+	// 状态机
+	Space(c) {
+		if (c == '.' || c == '#' || this.isLetter(c)) {
+			this.start = this.i;
+			this.state = this.Name;
+		} else if (c == '/' && this.data[this.i + 1] == '*')
+			this.Comment();
+		else if (!cfg.blankChar[c] && c != ';')
+			this.state = this.Ignore;
+	}
+	Comment() {
+		this.i = this.data.indexOf('*/', this.i) + 1;
+		if (!this.i) this.i = this.data.length;
+		this.state = this.Space;
+	}
+	Ignore(c) {
+		if (c == '{') this.floor++;
+		else if (c == '}' && !--this.floor) this.state = this.Space;
+	}
+	Name(c) {
+		if (cfg.blankChar[c]) {
+			this.list.push(this.section());
+			this.state = this.NameSpace;
+		} else if (c == '{') {
+			this.list.push(this.section());
+			this.Content();
+		} else if (c == ',') {
+			this.list.push(this.section());
+			this.Comma();
+		} else if (!this.isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
+			this.state = this.Ignore;
+	}
+	NameSpace(c) {
+		if (c == '{') this.Content();
+		else if (c == ',') this.Comma();
+		else if (!cfg.blankChar[c]) this.state = this.Ignore;
+	}
+	Comma() {
+		while (cfg.blankChar[this.data[++this.i]]);
+		if (this.data[this.i] == '{') this.Content();
+		else {
+			this.start = this.i--;
+			this.state = this.Name;
+		}
+	}
+	Content() {
+		this.start = ++this.i;
+		if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
+		var content = this.section();
+		for (var i = 0, item; item = this.list[i++];)
+			if (this.res[item]) this.res[item] += ';' + content;
+			else this.res[item] = content;
+		this.list = [];
+		this.state = this.Space;
+	}
+}

+ 577 - 0
components/jyf-parser/libs/MpHtmlParser.js

@@ -0,0 +1,577 @@
+/*
+  将 html 解析为适用于小程序 rich-text 的 DOM 结构
+  github:https://github.com/jin-yufeng/Parser
+  docs:https://jin-yufeng.github.io/Parser
+  author:JinYufeng
+  update:2020/04/13
+*/
+var cfg = require('./config.js'),
+	blankChar = cfg.blankChar,
+	CssHandler = require('./CssHandler.js'),
+	{
+		screenWidth,
+		system
+	} = wx.getSystemInfoSync();
+// #ifdef MP-BAIDU || MP-ALIPAY || MP-TOUTIAO
+var entities = {
+	lt: '<',
+	gt: '>',
+	amp: '&',
+	quot: '"',
+	apos: "'",
+	nbsp: '\xA0',
+	ensp: '\u2002',
+	emsp: '\u2003',
+	ndash: '–',
+	mdash: '—',
+	middot: '·',
+	lsquo: '‘',
+	rsquo: '’',
+	ldquo: '“',
+	rdquo: '”',
+	bull: '•',
+	hellip: '…',
+	permil: '‰',
+	copy: '©',
+	reg: '®',
+	trade: '™',
+	times: '×',
+	divide: '÷',
+	cent: '¢',
+	pound: '£',
+	yen: '¥',
+	euro: '€',
+	sect: '§'
+};
+// #endif
+var emoji; // emoji 补丁包 https://jin-yufeng.github.io/Parser/#/instructions?id=emoji
+class MpHtmlParser {
+	constructor(data, options = {}) {
+		this.attrs = {};
+		this.compress = options.compress;
+		this.CssHandler = new CssHandler(options.tagStyle, screenWidth);
+		this.data = data;
+		this.domain = options.domain;
+		this.DOM = [];
+		this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
+		this.protocol = this.domain && this.domain.includes('://') ? this.domain.split('://')[0] : '';
+		this.state = this.Text;
+		this.STACK = [];
+		this.useAnchor = options.useAnchor;
+		this.xml = options.xml;
+	}
+	parse() {
+		if (emoji) this.data = emoji.parseEmoji(this.data);
+		for (var c; c = this.data[this.i]; this.i++)
+			this.state(c);
+		if (this.state == this.Text) this.setText();
+		while (this.STACK.length) this.popNode(this.STACK.pop());
+		// #ifdef MP-BAIDU || MP-TOUTIAO
+		// 将顶层标签的一些样式提取出来给 rich-text
+		(function f(ns) {
+			for (var i = ns.length, n; n = ns[--i];) {
+				if (n.type == 'text') continue;
+				if (!n.c) {
+					var style = n.attrs.style;
+					if (style) {
+						var j, k, res;
+						if ((j = style.indexOf('display')) != -1)
+							res = style.substring(j, (k = style.indexOf(';', j)) == -1 ? style.length : k);
+						if ((j = style.indexOf('float')) != -1)
+							res += ';' + style.substring(j, (k = style.indexOf(';', j)) == -1 ? style.length : k);
+						n.attrs.contain = res;
+					}
+				} else f(n.children);
+			}
+		})(this.DOM);
+		// #endif
+		if (this.DOM.length) {
+			this.DOM[0].PoweredBy = 'Parser';
+			if (this.title) this.DOM[0].title = this.title;
+		}
+		return this.DOM;
+	}
+	// 设置属性
+	setAttr() {
+		var name = this.getName(this.attrName);
+		if (cfg.trustAttrs[name]) {
+			if (!this.attrVal) {
+				if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
+			} else if (name == 'src') this.attrs[name] = this.getUrl(this.attrVal.replace(/&amp;/g, '&'));
+			else this.attrs[name] = this.attrVal;
+		}
+		this.attrVal = '';
+		while (blankChar[this.data[this.i]]) this.i++;
+		if (this.isClose()) this.setNode();
+		else {
+			this.start = this.i;
+			this.state = this.AttrName;
+		}
+	}
+	// 设置文本节点
+	setText() {
+		var back, text = this.section();
+		if (!text) return;
+		text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
+		if (back) {
+			this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
+			let j = this.start + text.length;
+			for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
+			return;
+		}
+		if (!this.pre) {
+			// 合并空白符
+			var tmp = [];
+			for (let i = text.length, c; c = text[--i];)
+				if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c);
+			text = tmp.join('');
+			if (text == ' ') return;
+		}
+		// 处理实体
+		var siblings = this.siblings(),
+			i = -1,
+			j, en;
+		while (1) {
+			if ((i = text.indexOf('&', i + 1)) == -1) break;
+			if ((j = text.indexOf(';', i + 2)) == -1) break;
+			if (text[i + 1] == '#') {
+				en = parseInt((text[i + 2] == 'x' ? '0' : '') + text.substring(i + 2, j));
+				if (!isNaN(en)) text = text.substr(0, i) + String.fromCharCode(en) + text.substring(j + 1);
+			} else {
+				en = text.substring(i + 1, j);
+				// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS
+				if (en == 'nbsp') text = text.substr(0, i) + '\xA0' + text.substr(j + 1); // 解决 &nbsp; 失效
+				else if (en != 'lt' && en != 'gt' && en != 'amp' && en != 'ensp' && en != 'emsp' && en != 'quot' && en != 'apos') {
+					i && siblings.push({
+						type: 'text',
+						text: text.substr(0, i)
+					})
+					siblings.push({
+						type: 'text',
+						text: `&${en};`,
+						en: 1
+					})
+					text = text.substr(j + 1);
+					i = -1;
+				}
+				// #endif
+				// #ifdef MP-BAIDU || MP-ALIPAY || MP-TOUTIAO
+				if (entities[en]) text = text.substr(0, i) + entities[en] + text.substr(j + 1);
+				// #endif
+			}
+		}
+		text && siblings.push({
+			type: 'text',
+			text
+		})
+	}
+	// 设置元素节点
+	setNode() {
+		var node = {
+				name: this.tagName.toLowerCase(),
+				attrs: this.attrs
+			},
+			close = cfg.selfClosingTags[node.name] || (this.xml && this.data[this.i] == '/');
+		this.attrs = {};
+		if (!cfg.ignoreTags[node.name]) {
+			this.matchAttr(node);
+			if (!close) {
+				node.children = [];
+				if (node.name == 'pre' && cfg.highlight) {
+					this.remove(node);
+					this.pre = node.pre = true;
+				}
+				this.siblings().push(node);
+				this.STACK.push(node);
+			} else if (!cfg.filter || cfg.filter(node, this) != false)
+				this.siblings().push(node);
+		} else {
+			if (!close) this.remove(node);
+			else if (node.name == 'source') {
+				var parent = this.STACK[this.STACK.length - 1],
+					attrs = node.attrs;
+				if (parent && attrs.src)
+					if (parent.name == 'video' || parent.name == 'audio')
+						parent.attrs.source.push(attrs.src);
+					else {
+						var i, media = attrs.media;
+						if (parent.name == 'picture' && !parent.attrs.src && !(attrs.src.indexOf('.webp') && system.includes('iOS')) &&
+							(!media || (media.includes('px') &&
+								(((i = media.indexOf('min-width')) != -1 && (i = media.indexOf(':', i + 8)) != -1 && screenWidth > parseInt(
+										media.substr(i + 1))) ||
+									((i = media.indexOf('max-width')) != -1 && (i = media.indexOf(':', i + 8)) != -1 && screenWidth < parseInt(
+										media.substr(i + 1)))))))
+							parent.attrs.src = attrs.src;
+					}
+			} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
+		}
+		if (this.data[this.i] == '/') this.i++;
+		this.start = this.i + 1;
+		this.state = this.Text;
+	}
+	// 移除标签
+	remove(node) {
+		var name = node.name,
+			j = this.i;
+		while (1) {
+			if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
+				if (name == 'pre' || name == 'svg') this.i = j;
+				else this.i = this.data.length;
+				return;
+			}
+			this.start = (this.i += 2);
+			while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
+			if (this.getName(this.section()) == name) {
+				// 代码块高亮
+				if (name == 'pre') {
+					this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) +
+						this.data.substr(this.i - 5);
+					return this.i = j;
+				} else if (name == 'style')
+					this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
+				else if (name == 'title')
+					this.title = this.data.substring(j + 1, this.i - 7);
+				if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
+				// 处理 svg
+				if (name == 'svg') {
+					var src = this.data.substring(j, this.i + 1);
+					if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
+					var i = j;
+					while (this.data[j] != '<') j--;
+					src = this.data.substring(j, i) + src;
+					var parent = this.STACK[this.STACK.length - 1];
+					if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
+						parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
+					this.siblings().push({
+						name: 'img',
+						attrs: {
+							src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+							ignore: 'T'
+						}
+					})
+				}
+				return;
+			}
+		}
+	}
+	// 处理属性
+	matchAttr(node) {
+		var attrs = node.attrs,
+			style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
+			styleObj = {};
+		if (attrs.id) {
+			if (this.compress & 1) attrs.id = void 0;
+			else if (this.useAnchor) this.bubble();
+		}
+		if ((this.compress & 2) && attrs.class) attrs.class = void 0;
+		switch (node.name) {
+			case 'img':
+				if (attrs['data-src']) {
+					attrs.src = attrs.src || attrs['data-src'];
+					attrs['data-src'] = void 0;
+				}
+				if (attrs.src && !attrs.ignore) {
+					if (this.bubble()) attrs.i = (this.imgNum++).toString();
+					else attrs.ignore = 'T';
+				}
+				break;
+			case 'a':
+			case 'ad':
+			// #ifdef APP-PLUS
+			case 'iframe':
+			case 'embed':
+			// #endif
+				this.bubble();
+				break;
+			case 'font':
+				if (attrs.color) {
+					styleObj['color'] = attrs.color;
+					attrs.color = void 0;
+				}
+				if (attrs.face) {
+					styleObj['font-family'] = attrs.face;
+					attrs.face = void 0;
+				}
+				if (attrs.size) {
+					var size = parseInt(attrs.size);
+					if (size < 1) size = 1;
+					else if (size > 7) size = 7;
+					var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
+					styleObj['font-size'] = map[size - 1];
+					attrs.size = void 0;
+				}
+				break;
+			case 'video':
+			case 'audio':
+				if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
+				else this[`${node.name}Num`]++;
+				if (node.name == 'video') {
+					if (attrs.width) {
+						style = `width:${parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')};${style}`;
+						attrs.width = void 0;
+					}
+					if (attrs.height) {
+						style = `height:${parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')};${style}`;
+						attrs.height = void 0;
+					}
+					if (this.videoNum > 3) node.lazyLoad = true;
+				}
+				attrs.source = [];
+				if (attrs.src) attrs.source.push(attrs.src);
+				if (!attrs.controls && !attrs.autoplay)
+					console.warn(`存在没有 controls 属性的 ${node.name} 标签,可能导致无法播放`, node);
+				this.bubble();
+				break;
+			case 'td':
+			case 'th':
+				if (attrs.colspan || attrs.rowspan)
+					for (var k = this.STACK.length, item; item = this.STACK[--k];)
+						if (item.name == 'table') {
+							item.c = void 0;
+							break;
+						}
+		}
+		if (attrs.align) {
+			styleObj['text-align'] = attrs.align;
+			attrs.align = void 0;
+		}
+		// 压缩 style
+		var styles = style.replace(/&quot;/g, '"').replace(/&amp;/g, '&').split(';');
+		style = '';
+		for (var i = 0, len = styles.length; i < len; i++) {
+			var info = styles[i].split(':');
+			if (info.length < 2) continue;
+			let key = info[0].trim().toLowerCase(),
+				value = info.slice(1).join(':').trim();
+			if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value
+				.includes(
+					'safe'))
+				style += `;${key}:${value}`;
+			else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
+				styleObj[key] = value;
+		}
+		if (node.name == 'img' && parseInt(styleObj.width || attrs.width) > screenWidth)
+			styleObj.height = 'auto';
+		for (var key in styleObj) {
+			var value = styleObj[key];
+			if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
+			// 填充链接
+			if (value.includes('url')) {
+				var j = value.indexOf('(');
+				if (j++ != -1) {
+					while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
+					value = value.substr(0, j) + this.getUrl(value.substr(j));
+				}
+			}
+			// 转换 rpx
+			else if (value.includes('rpx'))
+				value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * screenWidth / 750 + 'px');
+			else if (key == 'white-space' && value.includes('pre'))
+				this.pre = node.pre = true;
+			style += `;${key}:${value}`;
+		}
+		style = style.substr(1);
+		if (style) attrs.style = style;
+	}
+	// 节点出栈处理
+	popNode(node) {
+		// 空白符处理
+		if (node.pre) {
+			node.pre = this.pre = void 0;
+			for (let i = this.STACK.length; i--;)
+				if (this.STACK[i].pre)
+					this.pre = true;
+		}
+		if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
+			return this.siblings().pop();
+		var attrs = node.attrs;
+		// 替换一些标签名
+		if (node.name == 'picture') {
+			node.name = 'img';
+			if (!attrs.src && (node.children[0] || '').name == 'img')
+				attrs.src = node.children[0].attrs.src;
+			if (attrs.src && !attrs.ignore)
+				attrs.i = (this.imgNum++).toString();
+			return node.children = void 0;
+		}
+		if (cfg.blockTags[node.name]) node.name = 'div';
+		else if (!cfg.trustTags[node.name]) node.name = 'span';
+		// 处理列表
+		if (node.c) {
+			if (node.name == 'ul') {
+				var floor = 1;
+				for (let i = this.STACK.length; i--;)
+					if (this.STACK[i].name == 'ul') floor++;
+				if (floor != 1)
+					for (let i = node.children.length; i--;)
+						node.children[i].floor = floor;
+			} else if (node.name == 'ol') {
+				for (let i = 0, num = 1, child; child = node.children[i++];)
+					if (child.name == 'li') {
+						child.type = 'ol';
+						child.num = ((num, type) => {
+							if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
+							if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
+							if (type == 'i' || type == 'I') {
+								num = (num - 1) % 99 + 1;
+								var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
+									ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
+									res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
+								if (type == 'i') return res.toLowerCase();
+								return res;
+							}
+							return num;
+						})(num++, attrs.type) + '.';
+					}
+			}
+		}
+		// 处理表格的边框
+		if (node.name == 'table') {
+			var padding = attrs.cellpadding,
+				spacing = attrs.cellspacing,
+				border = attrs.border;
+			if (node.c) {
+				this.bubble();
+				if (!padding) padding = 2;
+				if (!spacing) spacing = 2;
+			}
+			if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
+			if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
+			if (border || padding)
+				(function f(ns) {
+					for (var i = 0, n; n = ns[i]; i++) {
+						if (n.name == 'th' || n.name == 'td') {
+							if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style}`;
+							if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style}`;
+						} else f(n.children || []);
+					}
+				})(node.children)
+		}
+		this.CssHandler.pop && this.CssHandler.pop(node);
+		// 自动压缩
+		if (node.name == 'div' && !Object.keys(attrs).length) {
+			var siblings = this.siblings();
+			if (node.children.length == 1 && node.children[0].name == 'div')
+				siblings[siblings.length - 1] = node.children[0];
+		}
+	}
+	// 工具函数
+	bubble() {
+		for (var i = this.STACK.length, item; item = this.STACK[--i];) {
+			if (cfg.richOnlyTags[item.name]) {
+				if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
+				return false;
+			}
+			item.c = 1;
+		}
+		return true;
+	}
+	getName = val => this.xml ? val : val.toLowerCase();
+	getUrl(url) {
+		if (url[0] == '/') {
+			if (url[1] == '/') url = this.protocol + ':' + url;
+			else if (this.domain) url = this.domain + url;
+		} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
+			url = this.domain + '/' + url;
+		return url;
+	}
+	isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
+	section = () => this.data.substring(this.start, this.i);
+	siblings = () => this.STACK.length ? this.STACK[this.STACK.length - 1].children : this.DOM;
+	// 状态机
+	Text(c) {
+		if (c == '<') {
+			var next = this.data[this.i + 1],
+				isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+			if (isLetter(next)) {
+				this.setText();
+				this.start = this.i + 1;
+				this.state = this.TagName;
+			} else if (next == '/') {
+				this.setText();
+				if (isLetter(this.data[++this.i + 1])) {
+					this.start = this.i + 1;
+					this.state = this.EndTag;
+				} else
+					this.Comment();
+			} else if (next == '!') {
+				this.setText();
+				this.Comment();
+			}
+		}
+	}
+	Comment() {
+		var key;
+		if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
+		else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
+		else key = '>';
+		if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
+		else this.i += key.length - 1;
+		this.start = this.i + 1;
+		this.state = this.Text;
+	}
+	TagName(c) {
+		if (blankChar[c]) {
+			this.tagName = this.section();
+			while (blankChar[this.data[this.i]]) this.i++;
+			if (this.isClose()) this.setNode();
+			else {
+				this.start = this.i;
+				this.state = this.AttrName;
+			}
+		} else if (this.isClose()) {
+			this.tagName = this.section();
+			this.setNode();
+		}
+	}
+	AttrName(c) {
+		var blank = blankChar[c];
+		if (blank) {
+			this.attrName = this.section();
+			c = this.data[this.i];
+		}
+		if (c == '=') {
+			if (!blank) this.attrName = this.section();
+			while (blankChar[this.data[++this.i]]);
+			this.start = this.i--;
+			this.state = this.AttrValue;
+		} else if (blank) this.setAttr();
+		else if (this.isClose()) {
+			this.attrName = this.section();
+			this.setAttr();
+		}
+	}
+	AttrValue(c) {
+		if (c == '"' || c == "'") {
+			this.start++;
+			if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
+			this.attrVal = this.section();
+			this.i++;
+		} else {
+			for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
+			this.attrVal = this.section();
+		}
+		this.setAttr();
+	}
+	EndTag(c) {
+		if (blankChar[c] || c == '>' || c == '/') {
+			var name = this.getName(this.section());
+			for (var i = this.STACK.length; i--;)
+				if (this.STACK[i].name == name) break;
+			if (i != -1) {
+				var node;
+				while ((node = this.STACK.pop()).name != name);
+				this.popNode(node);
+			} else if (name == 'p' || name == 'br')
+				this.siblings().push({
+					name,
+					attrs: {}
+				});
+			this.i = this.data.indexOf('>', this.i);
+			this.start = this.i + 1;
+			if (this.i == -1) this.i = this.data.length;
+			else this.state = this.Text;
+		}
+	}
+}
+module.exports = MpHtmlParser;

+ 80 - 0
components/jyf-parser/libs/config.js

@@ -0,0 +1,80 @@
+/* 配置文件 */
+// #ifdef MP-WEIXIN
+const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
+// #endif
+module.exports = {
+	// 过滤器函数
+	filter: null,
+	// 代码高亮函数
+	highlight: null,
+	// 文本处理函数
+	onText: null,
+	blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
+	// 块级标签,将被转为 div
+	blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + (
+		// #ifdef MP-WEIXIN
+		canIUse ? '' :
+		// #endif
+		',pre')),
+	// 将被移除的标签
+	ignoreTags: makeMap(
+		'area,base,basefont,canvas,command,frame,input,isindex,keygen,link,map,meta,param,script,source,style,svg,textarea,title,track,use,wbr'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',rp' : '')
+		// #endif
+		// #ifndef APP-PLUS
+		+ ',embed,iframe'
+		// #endif
+	),
+	// 只能被 rich-text 显示的标签
+	richOnlyTags: makeMap('a,colgroup,fieldset,legend,picture,table'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
+		// #endif
+	),
+	// 自闭合的标签
+	selfClosingTags: makeMap(
+		'area,base,basefont,br,col,circle,ellipse,embed,frame,hr,img,input,isindex,keygen,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
+	),
+	// 信任的属性
+	trustAttrs: makeMap(
+		'align,alt,app-id,author,autoplay,border,cellpadding,cellspacing,class,color,colspan,controls,data-src,dir,face,height,href,id,ignore,loop,media,muted,name,path,poster,rowspan,size,span,src,start,style,type,unit-id,width,xmlns'
+	),
+	// bool 型的属性
+	boolAttrs: makeMap('autoplay,controls,ignore,loop,muted'),
+	// 信任的标签
+	trustTags: makeMap(
+		'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
+		// #endif
+		// #ifdef APP-PLUS
+		+ ',embed,iframe'
+		// #endif
+	),
+	// 默认的标签样式
+	userAgentStyles: {
+		address: 'font-style:italic',
+		big: 'display:inline;font-size:1.2em',
+		blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
+		caption: 'display:table-caption;text-align:center',
+		center: 'text-align:center',
+		cite: 'font-style:italic',
+		dd: 'margin-left:40px',
+		img: 'max-width:100%',
+		mark: 'background-color:yellow',
+		picture: 'max-width:100%',
+		pre: 'font-family:monospace;white-space:pre;overflow:scroll',
+		s: 'text-decoration:line-through',
+		small: 'display:inline;font-size:0.8em',
+		u: 'text-decoration:underline'
+	}
+}
+
+function makeMap(str) {
+	var map = {},
+		list = str.split(',');
+	for (var i = list.length; i--;)
+		map[list[i]] = true;
+	return map;
+}

+ 35 - 0
components/jyf-parser/libs/handler.sjs

@@ -0,0 +1,35 @@
+var inlineTags = {
+	abbr: 1,
+	b: 1,
+	big: 1,
+	code: 1,
+	del: 1,
+	em: 1,
+	i: 1,
+	ins: 1,
+	label: 1,
+	q: 1,
+	small: 1,
+	span: 1,
+	strong: 1
+}
+export default {
+	// 从顶层标签的样式中取出一些给 rich-text
+	getStyle: function(style) {
+		if (style) {
+			var i, j, res = '';
+			if ((i = style.indexOf('display')) != -1)
+				res = style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j);
+			if ((i = style.indexOf('float')) != -1)
+				res += ';' + style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j);
+			return res;
+		}
+	},
+	getNode: function(item) {
+		return [item];
+	},
+	// 是否通过 rich-text 显示
+	useRichText: function(item) {
+		return !item.c && !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1;
+	}
+}

+ 44 - 0
components/jyf-parser/libs/handler.wxs

@@ -0,0 +1,44 @@
+var inlineTags = {
+	abbr: 1,
+	b: 1,
+	big: 1,
+	code: 1,
+	del: 1,
+	em: 1,
+	i: 1,
+	ins: 1,
+	label: 1,
+	q: 1,
+	small: 1,
+	span: 1,
+	strong: 1
+}
+module.exports = {
+	// 从顶层标签的样式中取出一些给 rich-text
+	getStyle: function(style) {
+		if (style) {
+			var i, j, res = '';
+			if ((i = style.indexOf('display')) != -1)
+				res = style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j);
+			if ((i = style.indexOf('float')) != -1)
+				res += ';' + style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j);
+			return res;
+		}
+	},
+	// 处理懒加载
+	getNode: function(item, imgLoad) {
+		if (!imgLoad && item.attrs.i != '0') {
+			var img = {
+				name: 'img',
+				attrs: JSON.parse(JSON.stringify(item.attrs))
+			}
+			delete img.attrs.src;
+			img.attrs.style += ';width:20px;height:20px';
+			return [img];
+		} else return [item];
+	},
+	// 是否通过 rich-text 显示
+	useRichText: function(item) {
+		return !item.c && !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1;
+	}
+}

+ 476 - 0
components/jyf-parser/libs/trees.vue

@@ -0,0 +1,476 @@
+<!--
+  trees 递归显示组件
+  github:https://github.com/jin-yufeng/Parser 
+  docs:https://jin-yufeng.github.io/Parser
+  插件市场:https://ext.dcloud.net.cn/plugin?id=805
+  author:JinYufeng
+  update:2020/04/13
+-->
+<template>
+	<view class="interlayer">
+		<block v-for="(n, index) in nodes" v-bind:key="index">
+			<!--图片-->
+			<!--#ifdef MP-WEIXIN || MP-QQ || MP-ALIPAY || APP-PLUS-->
+			<rich-text v-if="n.name=='img'" :id="n.attrs.id" class="_img" :style="''+handler.getStyle(n.attrs.style)" :nodes="handler.getNode(n,!lazyLoad||imgLoad)"
+			 :data-attrs="n.attrs" @tap="imgtap" @longpress="imglongtap" />
+			<!--#endif-->
+			<!--#ifdef MP-BAIDU || MP-TOUTIAO-->
+			<rich-text v-if="n.name=='img'" :id="n.attrs.id" class="_img" :style="n.attrs.contain" :nodes='[n]' :data-attrs="n.attrs"
+			 @tap="imgtap" @longpress="imglongtap" />
+			<!--#endif-->
+			<!--文本-->
+			<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
+			<rich-text v-else-if="n.decode" class="_entity" :nodes="[n]"></rich-text>
+			<!--#endif-->
+			<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
+			<text v-else-if="n.name=='br'">\n</text>
+			<!--视频-->
+			<view v-else-if="n.name=='video'">
+				<view v-if="(!loadVideo||n.lazyLoad)&&!(controls[n.attrs.id]&&controls[n.attrs.id].play)" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
+				 :style="n.attrs.style" @tap="_loadVideo" />
+				<video v-else :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||(controls[n.attrs.id]&&controls[n.attrs.id].play)"
+				 :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[(controls[n.attrs.id]&&controls[n.attrs.id].index)||0]"
+				 :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" data-from="video" data-source="source" @error="error" @play="play" />
+			</view>
+			<!--音频-->
+			<audio v-else-if="n.name=='audio'" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :autoplay="n.attrs.autoplay"
+			 :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.attrs.source[(controls[n.attrs.id]&&controls[n.attrs.id].index)||0]"
+			 :data-id="n.attrs.id" data-from="audio" data-source="source" @error="error" @play="play" />
+			<!--链接-->
+			<view v-else-if="n.name=='a'" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
+			 :data-attrs="n.attrs" @tap="linkpress">
+				<trees class="_span" :nodes="n.children" />
+			</view>
+			<!--广告(按需打开注释)-->
+			<!--#ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO-->
+			<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']"
+			 data-from="ad" @error="error" />-->
+			<!--#endif-->
+			<!--#ifdef MP-BAIDU-->
+			<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :appid="n.attrs.appid"
+			 :apid="n.attrs.apid" :type="n.attrs.type" data-from="ad" @error="error" />-->
+			<!--#endif-->
+			<!--#ifdef APP-PLUS-->
+			<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :adpid="n.attrs.adpid"
+			 data-from="ad" @error="error" />-->
+			<!--#endif-->
+			<!--列表-->
+			<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex'">
+				<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
+				<view v-else class="_ul-bef">
+					<view v-if="n.floor%3==0" class="_ul-p1">█</view>
+					<view v-else-if="n.floor%3==2" class="_ul-p2" />
+					<view v-else class="_ul-p1" style="border-radius:50%">█</view>
+				</view>
+				<!--#ifdef MP-ALIPAY-->
+				<view class="_li">
+					<trees :nodes="n.children" />
+				</view>
+				<!--#endif-->
+				<!--#ifndef MP-ALIPAY-->
+				<trees class="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loadVideo="loadVideo" />
+				<!--#endif-->
+			</view>
+			<!--表格-->
+			<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
+				<view v-for="(tbody, i) in n.children" v-bind:key="i" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
+					<view v-for="(tr, j) in tbody.children" v-bind:key="j" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
+						<trees v-if="tr.name=='td'" :nodes="tr.children" :lazyLoad="lazyLoad" :loadVideo="loadVideo" />
+						<block v-else>
+							<!--#ifdef MP-ALIPAY-->
+							<view v-for="(td, k) in tr.children" v-bind:key="k" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')">
+								<trees :nodes="td.children" />
+							</view>
+							<!--#endif-->
+							<!--#ifndef MP-ALIPAY-->
+							<trees v-for="(td, k) in tr.children" v-bind:key="k" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
+							 :nodes="td.children" :lazyLoad="lazyLoad" :loadVideo="loadVideo" />
+							<!--#endif-->
+						</block>
+					</view>
+				</view>
+			</view>
+			<!--#ifdef APP-PLUS-->
+			<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
+			 :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+			<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+			<!--#endif-->
+			<!--富文本-->
+			<!--#ifdef MP-WEIXIN || MP-QQ || MP-ALIPAY || APP-PLUS-->
+			<rich-text v-else-if="handler.useRichText(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
+			<!--#endif-->
+			<!--#ifdef MP-BAIDU || MP-TOUTIAO-->
+			<rich-text v-else-if="!(n.c||n.continue)" :id="n.attrs.id" :class="_p" :style="n.attrs.contain" :nodes="[n]" />
+			<!--#endif-->
+			<!--#ifdef MP-ALIPAY-->
+			<view v-else :id="n.attrs.id" :class="'_'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style">
+				<trees :nodes="n.children" />
+			</view>
+			<!--#endif-->
+			<!--#ifndef MP-ALIPAY-->
+			<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style" :nodes="n.children"
+			 :lazyLoad="lazyLoad" :loadVideo="loadVideo" />
+			<!--#endif-->
+		</block>
+	</view>
+</template>
+<script module="handler" lang="wxs" src="./handler.wxs"></script>
+<script module="handler" lang="sjs" src="./handler.sjs"></script>
+<script>
+	global.Parser = {};
+	import trees from './trees'
+	export default {
+		components: {
+			trees
+		},
+		name: 'trees',
+		data() {
+			return {
+				controls: {},
+				// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS
+				imgLoad: false,
+				// #endif
+				// #ifndef APP-PLUS
+				loadVideo: true
+				// #endif
+			}
+		},
+		props: {
+			nodes: Array,
+			// #ifdef MP-WEIXIN || MP-QQ || H5 || APP-PLUS
+			lazyLoad: Boolean,
+			// #endif
+			// #ifdef APP-PLUS
+			loadVideo: Boolean
+			// #endif
+		},
+		mounted() {
+			// 获取顶层组件
+			this.top = this.$parent;
+			while (this.top.$options.name != 'parser') {
+				if (this.top.top) {
+					this.top = this.top.top;
+					break;
+				}
+				this.top = this.top.$parent;
+			}
+		},
+		// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS
+		beforeDestroy() {
+			if (this.observer)
+				this.observer.disconnect();
+		},
+		// #endif
+		methods: {
+			// #ifndef MP-ALIPAY
+			play(e) {
+				if (this.top.videoContexts.length > 1 && this.top.autopause)
+					for (var i = this.top.videoContexts.length; i--;)
+						if (this.top.videoContexts[i].id != e.currentTarget.dataset.id)
+							this.top.videoContexts[i].pause();
+			},
+			// #endif
+			imgtap(e) {
+				var attrs = e.currentTarget.dataset.attrs;
+				if (!attrs.ignore) {
+					var preview = true, data = {
+						id: e.target.id,
+						src: attrs.src,
+						ignore: () => preview = false
+					};
+					global.Parser.onImgtap && global.Parser.onImgtap(data);
+					this.top.$emit('imgtap', data);
+					if (preview) {
+						var urls = this.top.imgList,
+							current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
+						uni.previewImage({
+							current,
+							urls
+						})
+					}
+				}
+			},
+			imglongtap(e) {
+				var attrs = e.item.dataset.attrs;
+				if (!attrs.ignore)
+					this.top.$emit('imglongtap', {
+						id: e.target.id,
+						src: attrs.src
+					})
+			},
+			linkpress(e) {
+				var jump = true,
+					attrs = e.currentTarget.dataset.attrs;
+				attrs.ignore = () => jump = false;
+				global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
+				this.top.$emit('linkpress', attrs);
+				if (jump) {
+					// #ifdef MP
+					if (attrs['app-id']) {
+						return uni.navigateToMiniProgram({
+							appId: attrs['app-id'],
+							path: attrs.path
+						})
+					}
+					// #endif
+					if (attrs.href) {
+						if (attrs.href[0] == '#') {
+							if (this.top.useAnchor)
+								this.top.navigateTo({
+									id: attrs.href.substring(1)
+								})
+						} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
+							// #ifdef APP-PLUS
+							plus.runtime.openWeb(attrs.href);
+							// #endif
+							// #ifndef APP-PLUS
+							uni.setClipboardData({
+								data: attrs.href,
+								success: () =>
+									uni.showToast({
+										title: '链接已复制'
+									})
+							})
+							// #endif
+						} else
+							uni.navigateTo({
+								url: attrs.href
+							})
+					}
+				}
+			},
+			error(e) {
+				var context, target = e.currentTarget,
+					source = target.dataset.from;
+				if (source == 'video' || source == 'audio') {
+					// 加载其他 source
+					var index = this.controls[target.id] ? this.controls[target.id].index + 1 : 1;
+					if (index < target.dataset.source.length)
+						this.$set(this.controls, target.id + '.index', index);
+					if (source == 'video') context = uni.createVideoContext(target.id, this);
+				}
+				this.top && this.top.$emit('error', {
+					source,
+					target,
+					errMsg: e.detail.errMsg,
+					errCode: e.detail.errCode,
+					context
+				});
+			},
+			_loadVideo(e) {
+				this.$set(this.controls, e.currentTarget.id, {
+					play: true,
+					index: 0
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	/* 在这里引入自定义样式 */
+
+	/* 链接和图片效果 */
+	._a {
+		display: inline;
+		color: #366092;
+		word-break: break-all;
+		padding: 1.5px 0 1.5px 0;
+	}
+
+	._hover {
+		opacity: 0.7;
+		text-decoration: underline;
+	}
+
+	._img {
+		display: inline-block;
+		text-indent: 0;
+	}
+
+	/* #ifdef MP-WEIXIN */
+	:host {
+		display: inline;
+	}
+
+	/* #endif */
+
+	/* #ifdef MP */
+	.interlayer {
+		align-content: inherit;
+		align-items: inherit;
+		display: inherit;
+		flex-direction: inherit;
+		flex-wrap: inherit;
+		justify-content: inherit;
+		width: 100%;
+		white-space: inherit;
+	}
+
+	/* #endif */
+
+	._b,
+	._strong {
+		font-weight: bold;
+	}
+
+	._blockquote,
+	._div,
+	._p,
+	._ol,
+	._ul,
+	._li {
+		display: block;
+	}
+
+	._code {
+		font-family: monospace;
+	}
+
+	._del {
+		text-decoration: line-through;
+	}
+
+	._em,
+	._i {
+		font-style: italic;
+	}
+
+	._h1 {
+		font-size: 2em;
+	}
+
+	._h2 {
+		font-size: 1.5em;
+	}
+
+	._h3 {
+		font-size: 1.17em;
+	}
+
+	._h5 {
+		font-size: 0.83em;
+	}
+
+	._h6 {
+		font-size: 0.67em;
+	}
+
+	._h1,
+	._h2,
+	._h3,
+	._h4,
+	._h5,
+	._h6 {
+		display: block;
+		font-weight: bold;
+	}
+
+	._ins {
+		text-decoration: underline;
+	}
+
+	._li {
+		flex: 1;
+		width: 0;
+	}
+
+	._ol-bef {
+		margin-right: 5px;
+		text-align: right;
+		width: 36px;
+	}
+
+	._ul-bef {
+		line-height: normal;
+		margin: 0 12px 0 23px;
+	}
+
+	._ol-bef,
+	._ul_bef {
+		flex: none;
+		user-select: none;
+	}
+
+	._ul-p1 {
+		display: inline-block;
+		height: 0.3em;
+		line-height: 0.3em;
+		overflow: hidden;
+		width: 0.3em;
+	}
+
+	._ul-p2 {
+		border: 0.05em solid black;
+		border-radius: 50%;
+		display: inline-block;
+		height: 0.23em;
+		width: 0.23em;
+	}
+
+	._q::before {
+		content: '"';
+	}
+
+	._q::after {
+		content: '"';
+	}
+
+	._sub {
+		font-size: smaller;
+		vertical-align: sub;
+	}
+
+	._sup {
+		font-size: smaller;
+		vertical-align: super;
+	}
+
+	/* #ifndef MP-WEIXIN */
+	._abbr,
+	._b,
+	._code,
+	._del,
+	._em,
+	._i,
+	._ins,
+	._label,
+	._q,
+	._span,
+	._strong,
+	._sub,
+	._sup {
+		display: inline;
+	}
+
+	/* #endif */
+
+	/* #ifdef MP-WEIXIN || MP-QQ || MP-ALIPAY */
+	.__bdo,
+	.__bdi,
+	.__ruby,
+	.__rt,
+	._entity {
+		display: inline-block;
+	}
+
+	/* #endif */
+	._video {
+		background-color: black;
+		display: inline-block;
+		height: 225px;
+		position: relative;
+		width: 300px;
+	}
+
+	._video::after {
+		border-color: transparent transparent transparent white;
+		border-style: solid;
+		border-width: 15px 0 15px 30px;
+		content: '';
+		left: 50%;
+		margin: -15px 0 0 -15px;
+		position: absolute;
+		top: 50%;
+	}
+</style>

+ 105 - 0
components/pretty-times/pretty-times.scss

@@ -0,0 +1,105 @@
+.container{
+	view,text,image{
+		box-sizing: border-box;
+	}
+	scroll-view{
+		width: 100%;
+		white-space: nowrap;
+		height: 75px;
+		background-color: #fff;
+		position: relative;
+		padding-top: 10px;
+
+		// margin-top:10px;
+		&::after{
+			background: #e5e5e5;
+			content: '';
+			display:block;
+			width: 100%;
+			height: 1px;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			transform:scaleY(0.5);
+			
+		}
+		.flex-box{
+				display: inline-block;
+				height: 60px;
+				width: 25%;
+				margin: 0 7rpx 0 7rpx;
+				box-sizing: border-box;
+
+			&.active{
+				.date-box{
+					 border: none;
+					.days{
+						font-weight: bold;
+						color: #818181;
+					}
+					.date{
+						font-weight: bold;
+						color: #818181;
+					}
+				}
+			}
+			.date-box{	
+				border: none;
+				display: flex;
+				height: 50px;
+				flex-direction: column;
+				align-items: center;
+				justify-content: space-around;
+				font-size: 30upx;
+				color: rgba(129, 129, 129, 1);
+				.date{
+					font-weight: bold;
+					color: #818181;
+					font-size: 30upx;
+				
+				}
+			}
+		}
+		
+	}
+	.time-box{
+		padding:28upx 12upx 26upx;
+		display: flex;
+		flex-wrap: wrap;
+		// margin-top:10px;
+		background-color:#fff;
+		.item{
+			width: 33%;
+			padding: 0 9upx;
+			margin:10px 0;
+			&-box{
+				width: 100%;
+				height: 154upx;
+				padding:0 10upx;
+				background: #fff;
+				color: #333;
+				border: 1px solid #EEEEEE;
+				font-size: 28upx;
+				border-radius: 10upx;
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+				justify-content: center;
+				&.disable{
+					background: #F1F3F6 !important;
+					color: #999 !important;
+					// border: 1px solid #EEEEEE;
+				}
+				&.active{
+					// background: #0094D7;
+					font-weight: bold;
+				}
+				.all{
+					font-size: 22upx;
+					padding-top: 5px;
+				}
+			}
+		}
+	}
+	
+}

+ 322 - 0
components/pretty-times/pretty-times.vue

@@ -0,0 +1,322 @@
+<template>
+	<view class="content">
+		<view class="container">
+			<!-- 日期列表 -->
+			<scroll-view class="scroll-view_H b-t b-b" scroll-x>
+				<block v-for="(item,index) in dateArr" :key="index">
+					<div class="flex-box" @click="selectDateEvent(index,item)">
+						<view class="date-box" :style="{color:index==dateActive?selectedTabColor:'#333'}">
+							<text class="fontw">{{item.week}}</text>
+							<text>{{item.date}}</text>
+						</view>
+					</div>
+				</block>
+			</scroll-view>
+
+			<!-- 时间选项 -->
+			<view class="time-box">
+				<block v-for="(item,_index) in timeArr" :key="_index">
+					<view class="item">
+						<view class="item-box" :class="{'disable':item.disable,
+						'active':isMultiple?item.isActive:_index==timeActive}" :style="{color:isMultiple?item.isActive? selectedItemColor:'#333'
+						 :_index==timeActive?selectedItemColor:'#333'}" @click="selectTimeEvent(_index,item)">
+							<text>{{item.seltime}}</text>
+							<text class="all">{{item.disable?disableText:undisableText}}</text>
+						</view>
+					</view>
+				</block>
+			</view>
+		</view>
+		<view class="bottom">
+			<view class="show-time" v-if="!isMultiple">
+				预约时间:{{orderDateTime}}
+			</view>
+			<button form-type="submit" @click="handleSubmit" class="submit-btn" :style="'background:'+ stylecolor">确认预约</button>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		initData,
+		initTime,
+		timeStamp,
+		currentTime
+	} from '../utils/date.js'
+	export default {
+		name: 'times',
+		model: {
+			prop: "showPop",
+			event: "change"
+		},
+		props: {
+			technicalId: {
+				type: String,
+				default: ""
+			},
+			buynowinfoid: {
+				type: String,
+				default: ""
+			},
+			isMultiple: { //是否多选
+				type: Boolean,
+				default: false
+			},
+			
+			disableText: { //禁用显示的文本
+				type: String,
+				default: "已约满"
+			},
+			undisableText: { //未禁用显示的文本
+				type: String,
+				default: "可预约"
+			},
+			timeInterval: { // 时间间隔,小时为单位
+				type: Number,
+				default: 1
+			},
+			selectedTabColor: { // 日期栏选中的颜色
+				type: String,
+				default: "#ff1e02"
+			},
+			selectedItemColor: { // 时间选中的颜色
+				type: String,
+				default: "#ff1e02"
+			},
+			stylecolor:{
+				type: String,
+				default: '#ff1e02'
+			},
+			beginTime: {
+				type: String,
+				default: "09:00:00"
+			},
+			endTime: {
+				type: String,
+				default: "19:00:00"
+			},
+			appointTime: { // 预约的时间
+				type: Array,
+				default () {
+					return []
+				}
+			}
+		},
+		watch: {
+			appointTime(val) {
+				if (val && val.length) {
+					this.initOnload()
+				}
+			}
+		},
+		data() {
+			return {
+				orderDateTime: '暂无选择', // 选中时间
+				orderTimeArr: {}, //多选的时间
+				dateArr: [], //日期数据
+				timeArr: [], //时间数据
+				nowDate: "", // 当前日期
+				dateActive: 0, //选中的日期索引
+				timeActive: 0, //选中的时间索引
+				timeQuanBeginIndex: 0, //时间段开始的下标
+				selectDate: "", //选择的日期
+				selectTime: "", //选择的时间
+				timeQuanBegin: "", //时间段开始时间
+				timeQuanEnd: "", //时间段结束时间
+			}
+		},
+		mounted(props) {
+			this.selectDate = this.nowDate = currentTime().date
+			this.initOnload()
+		},
+		methods: {
+			initOnload() {
+				var _this = this;
+				_this.dateArr = initData() // 日期栏初始化
+				//_this.timeArr = initTime(_this.beginTime, _this.endTime, _this.timeInterval) //时间选项初始化
+				_this.$request.post('Servicetime.index', {
+					technicalId:_this.technicalId,
+					buynowinfoid:_this.buynowinfoid,
+					selectDate:_this.selectDate,
+					samkey: (new Date()).valueOf()
+				}).then(res => {
+					if (res.errno == 0) {
+						_this.timeArr = res.data;
+						_this.timeQuanBegin = _this.timeQuanEnd = ""
+
+						let isFullTime = true
+						_this.timeArr.forEach((item, index) => {
+
+							//判断是当前这一天,选中时间小于当前时间则禁用
+
+							if (_this.selectDate == _this.nowDate && currentTime().time > item.time) {
+								item.disable = true
+							}
+
+							// 将预约的时间禁用
+							_this.appointTime.forEach(t => {
+								let [date, time] = t.split(' ')
+								if (date == _this.selectDate && item.time == time) {
+									item.disable = true
+								}
+							})
+
+							// 判断是否当前日期时间都被预约
+							if (!item.disable) {
+								isFullTime = false
+							}
+						})
+
+						_this.orderDateTime = isFullTime ? "暂无选择" : _this.selectDate
+						_this.timeActive = -1
+						for (let i = 0, len = _this.timeArr.length; i < len; i++) {
+							if (!_this.timeArr[i].disable) {
+								_this.orderDateTime = `${_this.selectDate} ${_this.timeArr[i].seltime}`
+								_this.timeActive = i
+								return
+							}
+						}
+
+					}
+				})
+
+
+			},
+
+			// 日期选择事件
+			selectDateEvent(index, item) {
+				this.dateActive = index
+				this.selectDate = item.date
+				this.initOnload()
+			},
+
+			// 时间选择事件
+			selectTimeEvent(index, item) {
+				if (item.disable) return
+				if (this.isMultiple) {
+					item.isActive = !item.isActive
+					this.timeArr = this.timeArr.slice()
+					this.orderTimeArr[this.selectDate] = this.timeArr.reduce((prev, cur) => {
+						cur.isActive && prev.push(cur.time)
+						return prev
+					}, [])
+				} else {
+					this.timeActive = index
+					this.selectTime = item.seltime
+					this.orderDateTime = `${this.selectDate} ${item.seltime}`
+				}
+			},
+
+			// 选择时间段
+			handleSelectQuantum(index, item) {
+				if (item.disable) return
+
+				function clearTime() {
+					this.timeQuanBeginIndex = index
+					this.timeQuanBegin = item.time
+					this.timeQuanEnd = ""
+				}
+
+				if (!this.timeQuanBegin) {
+					clearTime.call(this)
+					return
+				}
+				if (!this.timeQuanEnd && this.timeQuanBegin) {
+					let isDisble = false
+					let start = this.timeQuanBeginIndex
+					let end = index
+					start > end && ([start, end] = [end, start])
+					for (let i = start + 1; i < end; i++) {
+						if (this.timeArr[i].disable) {
+							isDisble = true
+							clearTime.call(this)
+							return
+						}
+					}
+					if (!isDisble) {
+						for (let i = start + 1; i < end; i++) {
+							this.timeArr[i].isInclude = true
+						}
+					}
+					this.timeQuanEnd = item.time
+					return
+				}
+
+				if (this.timeQuanBegin && this.timeQuanEnd) {
+					this.timeArr.forEach(t => {
+						t.isInclude = false
+					})
+					clearTime.call(this)
+				}
+
+			},
+			handleChange() {
+				this.timeQuanBegin > this.timeQuanEnd && ([this.timeQuanBegin, this.timeQuanEnd] = [this.timeQuanEnd, this
+					.timeQuanBegin
+				])
+			},
+			handleSubmit() {
+				if (this.isMultiple) {
+					let time = []
+					for (let date in this.orderTimeArr) {
+						this.orderTimeArr[date].forEach(time => {
+							time.push(`${date} ${time}`)
+						})
+					}
+					this.$emit('change', time)
+				} else {
+					this.$emit('change', this.orderDateTime)
+				}
+
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	@import './pretty-times.scss';
+
+	page {
+		height: 100%;
+	}
+
+	.content {
+		text-align: center;
+		height: 100%;
+	}
+
+	/* 两个按钮 */
+	.bottom {
+		display: flex;
+		flex-direction: row;
+		position: fixed;
+		bottom: 8px;
+		top: auto;
+		left: 0px;
+		width: 100%;
+		background-color: #fff;
+	}
+
+	.show-time {
+		width: 70%;
+		height: 47px;
+		color: #505050;
+		background-color: rgba(255, 255, 255, 1);
+		font-size: 15px;
+		line-height: 47px;
+		text-align: center;
+	}
+	
+	.submit-btn {
+		width: 200rpx;
+		height: 70rpx;
+		line-height: 70rpx;
+		font-size: 28rpx;
+		border-radius: 50rpx;
+		color: #ffffff;
+		align-items: center;
+	}
+
+	.fontw {
+		font-weight: bold;
+	}
+</style>

+ 137 - 0
components/privacyPopup/privacyPopup.vue

@@ -0,0 +1,137 @@
+<template>
+	<view class="privacy" v-if="showPrivacy">
+		<view class="content">
+			<view class="title">隐私保护指引</view>
+			<view class="des">
+				在使用当前小程序服务之前,请仔细阅读
+				<text class="link" @click="openPrivacyContract">{{ privacyContractName }}</text>
+				如果你同意{{ privacyContractName }},请点击“同意”开始使用。如您拒绝,将无法更好的体验小程序。
+			</view>
+			<view class="btns">
+				<button class="item reject" @click="exitMiniProgram">拒绝</button>
+				<button id="agree-btn" class="item agree" open-type="agreePrivacyAuthorization" @agreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button>
+			</view>
+		</view>
+	</view>
+</template>
+ 
+<script>
+export default {
+	name: 'privacyPopup',
+	data() {
+		return {
+			privacyContractName: '',
+			showPrivacy: false
+		};
+	},
+	created() {
+		setTimeout(() => {
+			this.showPrivacy = getApp().globalData.showPrivacy;
+			this.privacyContractName = getApp().globalData.privacyContractName;
+		}, 500);
+	},
+	methods: {
+		// 同意隐私协议
+		handleAgreePrivacyAuthorization() {
+			const that = this;
+			wx.requirePrivacyAuthorize({
+				success: res => {
+					that.showPrivacy = false;
+					getApp().globalData.showPrivacy = false;
+				}
+			});
+		},
+		// 拒绝隐私协议
+		exitMiniProgram() {
+			const that = this;
+			uni.showModal({
+				content: '如果拒绝,我们将无法获取您的信息, 包括手机号、位置信息、相册等该小程序十分重要的功能,您确定要拒绝吗?',
+				success: res => {
+					if (res.confirm) {
+						that.showPrivacy = false;
+						uni.exitMiniProgram({
+							success: () => {
+								console.log('退出小程序成功');
+							}
+						});
+					}
+				}
+			});
+		},
+		// 跳转协议页面  
+        // 点击高亮的名字会自动跳转页面 微信封装好的不用操作
+		openPrivacyContract() {
+			wx.openPrivacyContract({
+				fail: () => {
+					uni.showToast({
+						title: '网络错误',
+						icon: 'error'
+					});
+				}
+			});
+		}
+	}
+};
+</script>
+ 
+<style lang="scss" scoped>
+.privacy {
+	position: fixed;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 9999999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	.content {
+		width: 85vw;
+		padding: 50rpx;
+		box-sizing: border-box;
+		background: #fff;
+		border-radius: 16rpx;
+		.title {
+			text-align: center;
+			color: #333;
+			font-weight: bold;
+			font-size: 34rpx;
+		}
+		.des {
+			font-size: 28rpx;
+			color: #666;
+			margin-top: 40rpx;
+			text-align: justify;
+			line-height: 1.6;
+			.link {
+				color: #07c160;
+			}
+		}
+		.btns {
+			margin-top: 60rpx;
+			display: flex;
+			justify-content: space-between;
+			.item {
+				justify-content: space-between;
+				width: 244rpx;
+				height: 80rpx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				border-radius: 16rpx;
+				box-sizing: border-box;
+				border: none;
+			}
+			.reject {
+				background: #f4f4f5;
+				color: #909399;
+			}
+			.agree {
+				background: #07c160;
+				color: #fff;
+			}
+		}
+	}
+}
+</style>

+ 185 - 0
components/thorui/tui-actionsheet/tui-actionsheet.vue

@@ -0,0 +1,185 @@
+<template>
+	<view @touchmove.stop.prevent>
+		<view class="tui-actionsheet" :class="{'tui-actionsheet-show':show,'tui-actionsheet-radius':radius}">
+			<view class="tui-actionsheet-tips" :style="{fontSize:size+'rpx',color:color}" v-if="tips">
+				{{tips}}
+			</view>
+			<view :class="[isCancel?'tui-operate-box':'']">
+				<block v-for="(item,index) in itemList" :key="index">
+					<view class="tui-actionsheet-btn tui-actionsheet-divider" :class="{'tui-btn-last':!isCancel && index==itemList.length-1}"
+					 hover-class="tui-actionsheet-hover" :hover-stay-time="150" :data-index="index" :style="{color:item.color || '#2B2B2B'}"
+					 @tap="handleClickItem">{{item.text}}</view>
+				</block>
+			</view>
+			<view class="tui-actionsheet-btn tui-actionsheet-cancel" hover-class="tui-actionsheet-hover" :hover-stay-time="150"
+			 v-if="isCancel" @tap="handleClickCancel">取消</view>
+		</view>
+		<view class="tui-actionsheet-mask" :class="{'tui-mask-show':show}" @tap="handleClickMask"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiActionsheet",
+		props: {
+			//点击遮罩 是否可关闭
+			maskClosable: {
+				type: Boolean,
+				default: true
+			},
+			//显示操作菜单
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//菜单按钮数组,自定义文本颜色,红色参考色:#e53a37
+			itemList: {
+				type: Array,
+				default: function() {
+					return [{
+						text: "确定",
+						color: "#2B2B2B"
+					}]
+				}
+			},
+			//提示文字
+			tips: {
+				type: String,
+				default: ""
+			},
+			//提示文字颜色
+			color: {
+				type: String,
+				default: "#808080"
+			},
+			//提示文字大小 rpx
+			size: {
+				type: Number,
+				default: 26
+			},
+			//是否需要圆角
+			radius: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要取消按钮
+			isCancel: {
+				type: Boolean,
+				default: true
+			}
+		},
+		methods: {
+			handleClickMask() {
+				if (!this.maskClosable) return;
+				this.handleClickCancel();
+			},
+			handleClickItem(e) {
+				if (!this.show) return;
+				const dataset = e.currentTarget.dataset;
+				this.$emit('click', {
+					index: Number(dataset.index)
+				});
+			},
+			handleClickCancel() {
+				this.$emit('cancel');
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-actionsheet {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 9999;
+		visibility: hidden;
+		transform: translate3d(0, 100%, 0);
+		transform-origin: center;
+		transition: all 0.25s ease-in-out;
+		background-color: #F7F7F7;
+		min-height: 100rpx;
+	}
+
+	.tui-actionsheet-radius {
+		border-top-left-radius: 20rpx;
+		border-top-right-radius: 20rpx;
+		overflow: hidden;
+	}
+
+	.tui-actionsheet-show {
+		transform: translate3d(0, 0, 0);
+		visibility: visible;
+	}
+
+	.tui-actionsheet-tips {
+		width: 100%;
+		padding: 40rpx 60rpx;
+		box-sizing: border-box;
+		text-align: center;
+		background-color: #fff;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-operate-box {
+		padding-bottom: 12rpx;
+	}
+
+	.tui-actionsheet-btn {
+		width: 100%;
+		height: 100rpx;
+		background-color: #fff;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		text-align: center;
+		font-size: 34rpx;
+		position: relative;
+	}
+
+	.tui-btn-last {
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+
+	.tui-actionsheet-divider::before {
+		content: '';
+		width: 100%;
+		border-top: 1rpx solid #E7E7E7;
+		position: absolute;
+		top: 0;
+		left: 0;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+	}
+
+	.tui-actionsheet-cancel {
+		color: #1a1a1a;
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+
+	.tui-actionsheet-hover {
+		background-color: #f7f7f9;
+	}
+
+	.tui-actionsheet-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.6);
+		z-index: 9996;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+</style>

+ 134 - 0
components/thorui/tui-alert/tui-alert.vue

@@ -0,0 +1,134 @@
+<template>
+	<view>
+		<view class="tui-alert-class tui-alert-box" :class="[show?'tui-alert-show':'']">
+			<view class="tui-alert-content" :style="{fontSize:size+'rpx',color:color}">
+				<slot></slot>
+			</view>
+			<view class="tui-alert-btn" :style="{color:btnColor}" hover-class="tui-alert-btn-hover" :hover-stay-time="150"
+			 @tap.stop="handleClick">{{btnText}}</view>
+		</view>
+		<view class="tui-alert-mask" :class="[show?'tui-alert-mask-show':'']" @tap.stop="handleClickCancel"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:"tuiAlert",
+		props: {
+			//控制显示
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//提示信息字体大小
+			size: {
+				type: Number,
+				default: 30
+			},
+			//提示信息字体颜色
+			color: {
+				type: String,
+				default: "#333"
+			},
+			//按钮字体颜色
+			btnColor: {
+				type: String,
+				default: "#EB0909"
+			},
+			btnText:{
+				type: String,
+				default: "确定"
+			},
+			//点击遮罩 是否可关闭
+			maskClosable: {
+				type: Boolean,
+				default: false
+			}
+		},
+		methods: {
+			handleClick(e) {
+				if (!this.show) return;
+				this.$emit('click', {});
+			},
+			handleClickCancel() {
+				if (!this.maskClosable) return;
+				this.$emit('cancel');
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-alert-box {
+		position: fixed;
+		width: 560rpx;
+		left: 50%;
+		top: 50%;
+		background-color: #fff;
+		transition: all 0.3s ease-in-out;
+		transform: translate(-50%, -50%) scale(0);
+		opacity: 0;
+		border-radius: 6rpx;
+		overflow: hidden;
+		z-index: 99998;
+	}
+
+	.tui-alert-show {
+		transform: translate(-50%, -50%) scale(1);
+		opacity: 1;
+	}
+
+	.tui-alert-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.5);
+		z-index: 99996;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-alert-mask-show {
+		visibility: visible;
+		opacity: 1;
+	}
+
+	.tui-alert-content {
+		text-align: center;
+		color: #333333;
+		padding: 98rpx 48rpx 92rpx 48rpx;
+		box-sizing: border-box;
+		word-break: break-all;
+	}
+
+	.tui-alert-btn {
+		width: 100%;
+		height: 90rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background-color: #fff;
+		box-sizing: border-box;
+		position: relative;
+		font-size: 32rpx;
+		line-height: 32rpx;
+	}
+
+	.tui-alert-btn-hover {
+		background-color: #f7f7f7;
+	}
+
+	.tui-alert-btn::before {
+		width: 100%;
+		content: "";
+		position: absolute;
+		border-top: 1rpx solid #E0E0E0;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		left: 0;
+		top: 0;
+	}
+</style>

+ 155 - 0
components/thorui/tui-badge/tui-badge.vue

@@ -0,0 +1,155 @@
+<template>
+	<view :class="[dot ? 'tui-badge-dot' : 'tui-badge', 'tui-' + type, !dot ? 'tui-badge-scale' : '']" :style="{ top: top, right: right, position: absolute ? 'absolute' : 'static', transform: getStyle, margin: margin }"
+	 @tap="handleClick">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiBadge',
+		props: {
+			//primary,warning,green,danger,white,black,gray,white_red
+			type: {
+				type: String,
+				default: 'primary'
+			},
+			//是否是圆点
+			dot: {
+				type: Boolean,
+				default: false
+			},
+			margin: {
+				type: String,
+				default: '0'
+			},
+			//是否绝对定位
+			absolute: {
+				type: Boolean,
+				default: false
+			},
+			top: {
+				type: String,
+				default: '-8rpx'
+			},
+			right: {
+				type: String,
+				default: '0'
+			},
+			//缩放比例
+			scaleRatio: {
+				type: Number,
+				default: 1
+			},
+			//水平方向移动距离
+			translateX: {
+				type: String,
+				default: '0'
+			}
+		},
+		computed: {
+			getStyle() {
+				return `scale(${this.scaleRatio}) translateX(${this.translateX})`;
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {});
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	/* color start*/
+
+	.tui-primary {
+		background-color: #5677fc;
+		color: #fff;
+	}
+
+	.tui-danger {
+		background-color: #ed3f14;
+		color: #fff;
+	}
+
+	.tui-red {
+		background-color: #F74D54;
+		color: #fff;
+	}
+
+	.tui-warning {
+		background-color: #ff7900;
+		color: #fff;
+	}
+
+	.tui-green {
+		background-color: #19be6b;
+		color: #fff;
+	}
+
+	.tui-white {
+		background-color: #fff;
+		color: #333;
+	}
+
+	.tui-white_red {
+		background-color: #fff;
+		color: #F74D54;
+	}
+
+	.tui-white_primary {
+		background-color: #fff;
+		color: #5677fc;
+	}
+
+	.tui-white_green {
+		background-color: #fff;
+		color: #19be6b;
+	}
+
+	.tui-white_warning {
+		background-color: #fff;
+		color: #ff7900;
+	}
+
+	.tui-black {
+		background-color: #000;
+		color: #fff;
+	}
+
+	.tui-gray {
+		background-color: #ededed;
+		color: #999;
+	}
+
+	/* color end*/
+
+	/* badge start*/
+
+	.tui-badge-dot {
+		height: 8px;
+		width: 8px;
+		border-radius: 50%;
+	}
+
+	.tui-badge {
+		font-size: 24rpx;
+		line-height: 24rpx;
+		height: 36rpx;
+		min-width: 36rpx;
+		padding: 0 10rpx;
+		box-sizing: border-box;
+		border-radius: 100rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		z-index: 10;
+	}
+
+	.tui-badge-scale {
+		transform-origin: center center;
+	}
+
+	/* badge end*/
+</style>

+ 387 - 0
components/thorui/tui-bottom-navigation/tui-bottom-navigation.vue

@@ -0,0 +1,387 @@
+<template>
+	<view @touchmove.stop.prevent="stop">
+		<view class="tui-bottom-navigation" :class="{ 'tui-navigation-fixed': isFixed, 'tui-remove-splitLine': unlined }">
+			<view
+				class="tui-navigation-item"
+				:class="{ 'tui-item-after_height': splitLineScale, 'tui-last-item': index == itemList.length - 1 }"
+				:style="{ backgroundColor: isDarkMode ? '#202020' : backgroundColor }"
+				v-for="(item, index) in itemList"
+				:key="index"
+			>
+				<view class="tui-item-inner" @tap="menuClick(index, item.parameter, item.type)">
+					<image
+						:src="current | getIcon(index, item)"
+						class="tui-navigation-img"
+						v-if="item.iconPath || (current == index && item.selectedIconPath && item.type == 1)"
+					></image>
+					<text
+						class="tui-navigation-text"
+						:style="{
+							color: isDarkMode ? '#fff' : current == index && item.type == 1 ? selectedColor : item.color || color,
+							fontWeight: current == index && bold && item.type == 1 ? 'bold' : 'normal',
+							fontSize: fontSize
+						}"
+					>
+						{{ item.text }}
+					</text>
+				</view>
+				<view
+					class="tui-navigation-popup"
+					:class="{ 'tui-navigation-popup_show': showMenuIndex == index }"
+					:style="{ backgroundColor: isDarkMode ? '#4c4c4c' : subMenuBgColor, left: item.popupLeft || '50%' }"
+					v-if="item.itemList"
+				>
+					<view
+						class="tui-popup-cell"
+						:class="{ 'tui-first-cell': subIndex === 0, 'tui-last-cell': subIndex === item.itemList.length - 1 }"
+						:hover-class="subMenuHover ? (isDarkMode ? 'tui-item-dark_hover' : 'tui-item-hover') : ''"
+						:hover-stay-time="150"
+						v-for="(subItem, subIndex) in item.itemList || []"
+						:key="subIndex"
+						@tap="subMenuClick(index, item.type, subIndex, subItem.parameter)"
+					>
+						<text class="tui-ellipsis" :style="{ color: isDarkMode ? '#fff' : subMenuColor, fontSize: subMenufontSize, lineHeight: subMenufontSize }">
+							{{ subItem.text }}
+						</text>
+					</view>
+					<view class="tui-popup-triangle" :style="{ borderTopColor: isDarkMode ? '#4c4c4c' : subMenuBgColor }"></view>
+				</view>
+			</view>
+		</view>
+		<view class="tui-navigation-mask" :class="{ 'tui-navigation-mask_show': showMenuIndex != -1 }" @tap="handleClose"></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiBottomNavigation',
+	props: {
+		//当前索引
+		current: {
+			type: Number,
+			default: 0
+		},
+		/**
+		 * {
+				text: 'ThorUI',
+				iconPath: '/static/images/common/icon_menu_gray.png',
+				selectedIconPath: '/static/images/common/icon_menu_gray.png',
+				color: '#666',
+				//1-选中切换,2-跳转、请求、其他操作,3-菜单
+				type: 3,
+				//自定义参数,类型自定义
+				parameter: null,
+				//子菜单left值,不传默认50%,当菜单贴近左右两边可用此参数调整
+				popupLeft: '',
+				itemList: [
+					{
+						//不建议超过6个字,请自行控制
+						text: '自定义参',
+						//自定义参数,类型自定义
+						parameter: null
+					},
+					{
+						text: '自定义参数',
+						//自定义参数,类型自定义
+						parameter: null
+					}
+				]
+			}
+		 * 
+		 * */
+		itemList: {
+			type: Array,
+			default: () => {
+				return [];
+			}
+		},
+		//颜色
+		color: {
+			type: String,
+			default: '#666'
+		},
+		//选中颜色
+		selectedColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		fontSize: {
+			type: String,
+			default: '28rpx'
+		},
+		//选中后字体是否加粗
+		bold: {
+			type: Boolean,
+			default: true
+		},
+		//导航条背景颜色
+		backgroundColor: {
+			type: String,
+			default: '#F8F8F8'
+		},
+		//item分割线高度是否缩小
+		splitLineScale: {
+			type: Boolean,
+			default: true
+		},
+		//二级菜单字体颜色
+		subMenuColor: {
+			type: String,
+			default: '#333'
+		},
+		//二级菜单字体大小
+		subMenufontSize: {
+			type: String,
+			default: '28rpx'
+		},
+		//二级菜单背景色  深色:#4c4c4c
+		subMenuBgColor: {
+			type: String,
+			default: '#fff'
+		},
+		//二级菜单是否有点击效果
+		subMenuHover: {
+			type: Boolean,
+			default: true
+		},
+		//是否固定在底部
+		isFixed: {
+			type: Boolean,
+			default: true
+		},
+		//去除导航栏顶部的线条
+		unlined: {
+			type: Boolean,
+			default: false
+		},
+		//是否暗黑模式 (true:所有设置颜色失效)
+		isDarkMode: {
+			type: Boolean,
+			default: false
+		}
+	},
+	filters: {
+		getIcon: function(current, index, item) {
+			let url = item.iconPath;
+			if (item.type == 1) {
+				url = current == index ? item.selectedIconPath || item.iconPath : item.iconPath;
+			}
+			return url;
+		}
+	},
+	data() {
+		return {
+			showMenuIndex: -1 //显示的菜单index
+		};
+	},
+	methods: {
+		stop() {
+			return false;
+		},
+		handleClose() {
+			this.showMenuIndex = -1;
+		},
+		menuClick(index, parameter, type) {
+			//type:1-选中切换,2-跳转、请求、其他操作,3-菜单
+			if (type == 3) {
+				this.showMenuIndex = this.showMenuIndex == index ? -1 : index;
+			} else {
+				this.showMenuIndex = -1;
+				this.$emit('click', {
+					menu: 'main', //main,sub 主菜单,子菜单
+					type: type,
+					index: index,
+					parameter: parameter || ''
+				});
+			}
+		},
+		subMenuClick(index, type, subIndex, parameter) {
+			this.showMenuIndex = -1;
+			this.$emit('click', {
+				menu: 'sub', //main,sub 主菜单,子菜单
+				type: type,
+				index: index,
+				subIndex: subIndex,
+				parameter: parameter || ''
+			});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-bottom-navigation {
+	width: 100%;
+	height: 100rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	position: relative;
+	z-index: 999;
+}
+
+.tui-navigation-fixed {
+	position: fixed !important;
+	left: 0;
+	bottom: 0;
+	padding-bottom: env(safe-area-inset-bottom);
+}
+
+.tui-bottom-navigation::after {
+	content: '';
+	width: 100%;
+	border-top: 1rpx solid #bfbfbf;
+	position: absolute;
+	top: 0;
+	left: 0;
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 0;
+	z-index: 1000;
+}
+.tui-remove-splitLine::before {
+	border-top: 0 !important;
+}
+
+.tui-navigation-item {
+	flex: 1;
+	height: 100rpx;
+	position: relative;
+	box-sizing: border-box;
+}
+
+.tui-item-inner {
+	width: 100%;
+	height: 100rpx;
+	display: flex;
+	text-align: center;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-navigation-item::after {
+	height: 100%;
+	content: '';
+	position: absolute;
+	border-right: 1rpx solid #bfbfbf;
+	transform: scaleX(0.5) translateZ(0);
+	right: 0;
+	top: 0;
+}
+
+.tui-item-after_height::after {
+	height: 40% !important;
+	top: 30% !important;
+}
+
+.tui-last-item::after {
+	border-right: 0 !important;
+}
+
+.tui-navigation-img {
+	width: 32rpx;
+	height: 32rpx;
+	margin-right: 8rpx;
+}
+
+.tui-navigation-popup {
+	max-width: 160%;
+	width: auto;
+	position: absolute;
+	border-radius: 8rpx;
+	visibility: hidden;
+	opacity: 0;
+	transform: translate3d(-50%, 0, 0);
+	transform-origin: center;
+	transition: all 0.12s ease-in-out;
+	bottom: 0;
+	z-index: -1;
+}
+
+.tui-navigation-popup_show {
+	transform: translate3d(-50%, -124rpx, 0);
+	visibility: visible;
+	opacity: 1;
+}
+
+.tui-popup-triangle {
+	position: absolute;
+	width: 0;
+	height: 0;
+	border-left: 9rpx solid transparent;
+	border-right: 9rpx solid transparent;
+	border-top: 18rpx solid;
+	left: 50%;
+	bottom: -18rpx;
+	-webkit-transform: translateX(-50%);
+	transform: translateX(-50%);
+	z-index: 997;
+}
+
+.tui-popup-cell {
+	width: 100%;
+	padding: 32rpx 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex: 1;
+	position: relative;
+}
+
+.tui-ellipsis {
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.tui-popup-cell::after {
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+	bottom: 0;
+	right: 24rpx;
+	left: 24rpx;
+}
+
+.tui-item-hover {
+	background-color: #f1f1f1;
+}
+
+.tui-item-dark_hover {
+	background-color: #555;
+}
+
+.tui-first-cell {
+	border-top-left-radius: 8rpx;
+	border-top-right-radius: 8rpx;
+}
+
+.tui-last-cell {
+	border-bottom-left-radius: 8rpx;
+	border-bottom-right-radius: 8rpx;
+}
+
+.tui-last-cell::after {
+	border-bottom: 0 !important;
+}
+
+.tui-navigation-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 995;
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	visibility: hidden;
+	background-color: rgba(0, 0, 0, 0);
+}
+
+.tui-navigation-mask_show {
+	opacity: 1;
+	visibility: visible;
+}
+</style>

+ 106 - 0
components/thorui/tui-bottom-popup/tui-bottom-popup.vue

@@ -0,0 +1,106 @@
+<template>
+	<view @touchmove.stop.prevent>
+		<view class="tui-popup-class tui-bottom-popup" :class="{ 'tui-popup-show': show, 'tui-popup-radius': radius }" :style="{ backgroundColor: backgroundColor, height: height ? height + 'rpx' : 'auto', zIndex: zIndex,transform:`translate3d(0, ${show?translateY:'100%'}, 0)`}">
+			<slot></slot>
+		</view>
+		<view class="tui-popup-mask" :class="[show ? 'tui-mask-show' : '']" :style="{ zIndex: maskZIndex }" v-if="mask" @tap="handleClose"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiBottomPopup',
+		props: {
+			//是否需要mask
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			//控制显示
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//背景颜色
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			//高度 rpx
+			height: {
+				type: Number,
+				default: 0
+			},
+			//设置圆角
+			radius: {
+				type: Boolean,
+				default: true
+			},
+			zIndex: {
+				type: [Number, String],
+				default: 997
+			},
+			maskZIndex: {
+				type: [Number, String],
+				default: 996
+			},
+			//弹层显示时,垂直方向移动的距离
+			translateY: {
+				type: String,
+				default: '0'
+			}
+		},
+		methods: {
+			handleClose() {
+				if (!this.show) {
+					return;
+				}
+				this.$emit('close', {});
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-bottom-popup {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		opacity: 0;
+		transform: translate3d(0, 100%, 0);
+		transform-origin: center;
+		transition: all 0.3s ease-in-out;
+		min-height: 20rpx;
+	}
+
+	.tui-popup-radius {
+		border-top-left-radius: 24rpx;
+		border-top-right-radius: 24rpx;
+		padding-bottom: env(safe-area-inset-bottom);
+		overflow: hidden;
+	}
+
+	.tui-popup-show {
+		opacity: 1;
+		/* transform: translate3d(0, 0, 0); */
+	}
+
+	.tui-popup-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.6);
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+</style>

+ 203 - 0
components/thorui/tui-bubble-popup/tui-bubble-popup.vue

@@ -0,0 +1,203 @@
+<template>
+	<view :class="{ 'tui-flex-end': flexEnd }">
+		<view class="tui-popup-list" :class="{ 'tui-popup-show': show,'tui-z_index':show && position!='relative' }" :style="{ width: width, backgroundColor: backgroundColor, borderRadius: radius, color: color, position: position, left: left, right: right, bottom: bottom, top: top,transform:`translate(${translateX},${translateY})` }">
+			<view class="tui-triangle" :style="{
+					borderWidth: borderWidth,
+					borderColor: `transparent transparent ${backgroundColor} transparent`,
+					left: triangleLeft,
+					right: triangleRight,
+					top: triangleTop,
+					bottom: triangleBottom
+				}"
+			 v-if="direction == 'top'"></view>
+			<view class="tui-triangle" :style="{
+					borderWidth: borderWidth,
+					borderColor: `${backgroundColor}  transparent transparent transparent`,
+					left: triangleLeft,
+					right: triangleRight,
+					top: triangleTop,
+					bottom: triangleBottom
+				}"
+			 v-if="direction == 'bottom'"></view>
+			<view class="tui-triangle" :style="{
+					borderWidth: borderWidth,
+					borderColor: `transparent  ${backgroundColor} transparent transparent`,
+					left: triangleLeft,
+					right: triangleRight,
+					top: triangleTop,
+					bottom: triangleBottom
+				}"
+			 v-if="direction == 'left'"></view>
+			<view class="tui-triangle" :style="{
+					borderWidth: borderWidth,
+					borderColor: `transparent transparent  transparent ${backgroundColor}`,
+					left: triangleLeft,
+					right: triangleRight,
+					top: triangleTop,
+					bottom: triangleBottom
+				}"
+			 v-if="direction == 'right'"></view>
+			<slot />
+		</view>
+		<view @touchmove.stop.prevent="stop" class="tui-popup-mask" :class="{ 'tui-popup-show': show }" :style="{ backgroundColor: maskBgColor }"
+		 v-if="mask" @tap="handleClose"></view>
+	</view>
+</template>
+<script>
+	export default {
+		name: 'tuiBubblePopup',
+		props: {
+			//宽度
+			width: {
+				type: String,
+				default: '300rpx'
+			},
+			//popup圆角
+			radius: {
+				type: String,
+				default: '8rpx'
+			},
+			//popup 定位 left right top bottom值
+			left: {
+				type: String,
+				default: 'auto'
+			},
+			right: {
+				type: String,
+				default: 'auto'
+			},
+			top: {
+				type: String,
+				default: 'auto'
+			},
+			bottom: {
+				type: String,
+				default: 'auto'
+			},
+			translateX:{
+				type: String,
+				default: '0'
+			},
+			translateY:{
+				type: String,
+				default: '0'
+			},
+			//背景颜色
+			backgroundColor: {
+				type: String,
+				default: '#4c4c4c'
+			},
+			//字体颜色
+			color: {
+				type: String,
+				default: '#fff'
+			},
+			//三角border-width
+			borderWidth: {
+				type: String,
+				default: '12rpx'
+			},
+			//三角形方向 top left right bottom
+			direction: {
+				type: String,
+				default: 'top'
+			},
+			//定位 left right top bottom值
+			triangleLeft: {
+				type: String,
+				default: 'auto'
+			},
+			triangleRight: {
+				type: String,
+				default: 'auto'
+			},
+			triangleTop: {
+				type: String,
+				default: 'auto'
+			},
+			triangleBottom: {
+				type: String,
+				default: 'auto'
+			},
+			//定位 relative absolute  fixed
+			position: {
+				type: String,
+				default: 'fixed'
+			},
+			//flex-end
+			flexEnd: {
+				type: Boolean,
+				default: false
+			},
+			//是否需要mask
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			maskBgColor: {
+				type: String,
+				default: 'rgba(0, 0, 0, 0.4)'
+			},
+			//控制显示
+			show: {
+				type: Boolean,
+				default: false
+			}
+		},
+		methods: {
+			handleClose() {
+				if (!this.show) {
+					return;
+				}
+				this.$emit('close', {});
+			},
+			stop() {
+				return false;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-popup-list {
+		z-index: 1;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-flex-end {
+		width: 100%;
+		display: flex;
+		justify-content: flex-end;
+	}
+
+	.tui-triangle {
+		position: absolute;
+		width: 0;
+		height: 0;
+		border-style: solid;
+		z-index: 997;
+	}
+
+	.tui-popup-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 995;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-popup-show {
+		opacity: 1;
+		visibility: visible;
+	}
+
+	.tui-z_index {
+		z-index: 996;
+	}
+</style>

+ 519 - 0
components/thorui/tui-button/tui-button.vue

@@ -0,0 +1,519 @@
+<template>
+	<button
+		class="tui-btn"
+		:class="[
+			plain ? 'tui-' + type + '-outline' : 'tui-btn-' + (type || 'primary'),
+			getDisabledClass(disabled, type, plain),
+			getShapeClass(shape, plain),
+			getShadowClass(type, shadow, plain),
+			bold ? 'tui-text-bold' : '',
+			link ? 'tui-btn__link' : ''
+		]"
+		:hover-class="getHoverClass(disabled, type, plain)"
+		:style="{ width: width, height: height, lineHeight: height, fontSize: size + 'rpx', margin: margin }"
+		:loading="loading"
+		:form-type="formType"
+		:open-type="openType"
+		@getuserinfo="bindgetuserinfo"
+		@getphonenumber="bindgetphonenumber"
+		@contact="bindcontact"
+		@error="binderror"
+		:disabled="disabled"
+		@tap="handleClick"
+	>
+		<slot></slot>
+	</button>
+</template>
+
+<script>
+export default {
+	name: 'tuiButton',
+	// #ifndef MP-QQ
+	behaviors: ['wx://form-field-button'],
+	// #endif
+	props: {
+		//样式类型 primary, white, danger, warning, green,blue, gray,black,brown,gray-primary,gray-danger,gray-warning,gray-green
+		type: {
+			type: String,
+			default: 'primary'
+		},
+		//是否加阴影
+		shadow: {
+			type: Boolean,
+			default: false
+		},
+		// 宽度 rpx或 %
+		width: {
+			type: String,
+			default: '100%'
+		},
+		//高度 rpx
+		height: {
+			type: String,
+			default: '96rpx'
+		},
+		//字体大小 rpx
+		size: {
+			type: Number,
+			default: 32
+		},
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		margin: {
+			type: String,
+			default: '0'
+		},
+		//形状 circle(圆角), square(默认方形),rightAngle(平角)
+		shape: {
+			type: String,
+			default: 'square'
+		},
+		plain: {
+			type: Boolean,
+			default: false
+		},
+		//link样式,去掉边框,结合plain一起使用
+		link: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		//禁用后背景是否为灰色 (非空心button生效)
+		disabledGray: {
+			type: Boolean,
+			default: false
+		},
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		formType: {
+			type: String,
+			default: ''
+		},
+		openType: {
+			type: String,
+			default: ''
+		},
+		index: {
+			type: [Number, String],
+			default: 0
+		},
+		//是否需要阻止重复点击【默认200ms】
+		preventClick: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {
+			time: 0
+		};
+	},
+	methods: {
+		handleClick() {
+			if (this.disabled) return;
+			if (this.preventClick) {
+				if(new Date().getTime() - this.time <= 200) return;
+				this.time = new Date().getTime();
+				setTimeout(() => {
+					this.time = 0;
+				}, 200);
+			}
+			this.$emit('click', {
+				index: Number(this.index)
+			});
+		},
+		bindgetuserinfo({ detail = {} } = {}) {
+			this.$emit('getuserinfo', detail);
+		},
+		bindcontact({ detail = {} } = {}) {
+			this.$emit('contact', detail);
+		},
+		bindgetphonenumber({ detail = {} } = {}) {
+			this.$emit('getphonenumber', detail);
+		},
+		binderror({ detail = {} } = {}) {
+			this.$emit('error', detail);
+		},
+		getShadowClass: function(type, shadow, plain) {
+			let className = '';
+			if (shadow && type != 'white' && !plain) {
+				className = 'tui-shadow-' + type;
+			}
+			return className;
+		},
+		getDisabledClass: function(disabled, type, plain) {
+			let className = '';
+			if (disabled && type != 'white' && type.indexOf('-') == -1) {
+				let classVal = this.disabledGray ? 'tui-gray-disabled' : 'tui-dark-disabled';
+				className = plain ? 'tui-dark-disabled-outline' : classVal;
+			}
+			return className;
+		},
+		getShapeClass: function(shape, plain) {
+			let className = '';
+			if (shape == 'circle') {
+				className = plain ? 'tui-outline-fillet' : 'tui-fillet';
+			} else if (shape == 'rightAngle') {
+				className = plain ? 'tui-outline-rightAngle' : 'tui-rightAngle';
+			}
+			return className;
+		},
+		getHoverClass: function(disabled, type, plain) {
+			let className = '';
+			if (!disabled) {
+				className = plain ? 'tui-outline-hover' : 'tui-' + (type || 'primary') + '-hover';
+			}
+			return className;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-btn-primary {
+	background: #5677fc !important;
+	color: #fff;
+}
+
+.tui-shadow-primary {
+	box-shadow: 0 10rpx 14rpx 0 rgba(86, 119, 252, 0.2);
+}
+
+.tui-btn-danger {
+	background: #eb0909 !important;
+	color: #fff;
+}
+
+.tui-shadow-danger {
+	box-shadow: 0 10rpx 14rpx 0 rgba(235, 9, 9, 0.2);
+}
+
+.tui-btn-warning {
+	background: #fc872d !important;
+	color: #fff;
+}
+
+.tui-shadow-warning {
+	box-shadow: 0 10rpx 14rpx 0 rgba(252, 135, 45, 0.2);
+}
+
+.tui-btn-green {
+	background: #07c160 !important;
+	color: #fff;
+}
+
+.tui-shadow-green {
+	box-shadow: 0 10rpx 14rpx 0 rgba(7, 193, 96, 0.2);
+}
+
+.tui-btn-blue {
+	background: #007aff !important;
+	color: #fff;
+}
+
+.tui-shadow-blue {
+	box-shadow: 0 10rpx 14rpx 0 rgba(0, 122, 255, 0.2);
+}
+
+.tui-btn-white {
+	background: #fff !important;
+	color: #333 !important;
+}
+
+.tui-btn-gray {
+	background: #bfbfbf !important;
+	color: #fff !important;
+}
+
+.tui-btn-black {
+	background: #333 !important;
+	color: #fff !important;
+}
+.tui-btn-brown{
+	background: #ac9157 !important;
+	color: #fff !important;
+}
+
+.tui-btn-gray-black {
+	background: #f2f2f2 !important;
+	color: #333;
+}
+
+.tui-btn-gray-primary {
+	background: #f2f2f2 !important;
+	color: #5677fc !important;
+}
+
+.tui-gray-primary-hover {
+	background: #d9d9d9 !important;
+}
+
+.tui-btn-gray-green {
+	background: #f2f2f2 !important;
+	color: #07c160 !important;
+}
+
+.tui-gray-green-hover {
+	background: #d9d9d9 !important;
+}
+
+.tui-btn-gray-danger {
+	background: #f2f2f2 !important;
+	color: #eb0909 !important;
+}
+
+.tui-gray-danger-hover {
+	background: #d9d9d9 !important;
+}
+
+.tui-btn-gray-warning {
+	background: #f2f2f2 !important;
+	color: #fc872d !important;
+}
+
+.tui-gray-warning-hover {
+	background: #d9d9d9 !important;
+}
+
+.tui-shadow-gray {
+	box-shadow: 0 10rpx 14rpx 0 rgba(191, 191, 191, 0.2);
+}
+
+.tui-hover-gray {
+	background: #f7f7f9 !important;
+}
+
+.tui-black-hover {
+	background: #555 !important;
+	color: #e5e5e5 !important;
+}
+.tui-brown-hover{
+	background: #A37F49 !important;
+	color: #e5e5e5 !important;
+}
+
+/* button start*/
+
+.tui-btn {
+	width: 100%;
+	position: relative;
+	border: 0 !important;
+	border-radius: 6rpx;
+	padding-left: 0;
+	padding-right: 0;
+	overflow: visible;
+}
+
+.tui-btn::after {
+	content: '';
+	position: absolute;
+	width: 200%;
+	height: 200%;
+	transform-origin: 0 0;
+	transform: scale(0.5, 0.5) translateZ(0);
+	box-sizing: border-box;
+	left: 0;
+	top: 0;
+	border-radius: 12rpx;
+	border: 0;
+}
+
+.tui-text-bold {
+	font-weight: bold;
+}
+
+.tui-btn-white::after {
+	border: 1px solid #bfbfbf;
+}
+
+.tui-white-hover {
+	background: #e5e5e5 !important;
+	color: #2e2e2e !important;
+}
+
+.tui-dark-disabled {
+	opacity: 0.6 !important;
+	color: #fafbfc !important;
+}
+
+.tui-dark-disabled-outline {
+	opacity: 0.5 !important;
+}
+
+.tui-gray-disabled {
+	background: #f3f3f3 !important;
+	color: #919191 !important;
+	box-shadow: none;
+}
+
+.tui-outline-hover {
+	opacity: 0.5;
+}
+
+.tui-primary-hover {
+	background: #4a67d6 !important;
+	color: #e5e5e5 !important;
+}
+
+.tui-primary-outline::after {
+	border: 1px solid #5677fc !important;
+}
+
+.tui-primary-outline {
+	color: #5677fc !important;
+	background: transparent;
+}
+
+.tui-danger-hover {
+	background: #c80808 !important;
+	color: #e5e5e5 !important;
+}
+
+.tui-danger-outline {
+	color: #eb0909 !important;
+	background: transparent;
+}
+
+.tui-danger-outline::after {
+	border: 1px solid #eb0909 !important;
+}
+
+.tui-warning-hover {
+	background: #d67326 !important;
+	color: #e5e5e5 !important;
+}
+
+.tui-warning-outline {
+	color: #fc872d !important;
+	background: transparent;
+}
+
+.tui-warning-outline::after {
+	border: 1px solid #fc872d !important;
+}
+
+.tui-green-hover {
+	background: #06ad56 !important;
+	color: #e5e5e5 !important;
+}
+
+.tui-green-outline {
+	color: #07c160 !important;
+	background: transparent;
+}
+
+.tui-green-outline::after {
+	border: 1px solid #07c160 !important;
+}
+
+.tui-blue-hover {
+	background: #0062cc !important;
+	color: #e5e5e5 !important;
+}
+
+.tui-blue-outline {
+	color: #007aff !important;
+	background: transparent;
+}
+
+.tui-blue-outline::after {
+	border: 1px solid #007aff !important;
+}
+
+/* #ifndef APP-NVUE */
+.tui-btn-gradual {
+	background: linear-gradient(90deg, rgb(255, 89, 38), rgb(240, 14, 44)) !important;
+	color: #fff !important;
+}
+
+.tui-shadow-gradual {
+	box-shadow: 0 10rpx 14rpx 0 rgba(235, 9, 9, 0.15);
+}
+
+/* #endif */
+
+.tui-gray-hover {
+	background: #a3a3a3 !important;
+	color: #898989;
+}
+
+/* #ifndef APP-NVUE */
+.tui-gradual-hover {
+	background: linear-gradient(90deg, #d74620, #cd1225) !important;
+	color: #fff !important;
+}
+
+/* #endif */
+
+.tui-gray-outline {
+	color: #999 !important;
+	background: transparent !important;
+}
+
+.tui-white-outline {
+	color: #fff !important;
+	background: transparent !important;
+}
+
+.tui-black-outline {
+	background: transparent !important;
+	color: #333 !important;
+}
+
+.tui-gray-outline::after {
+	border: 1px solid #ccc !important;
+}
+
+.tui-white-outline::after {
+	border: 1px solid #fff !important;
+}
+
+.tui-black-outline::after {
+	border: 1px solid #333 !important;
+}
+
+.tui-brown-outline {
+	color: #ac9157 !important;
+	background: transparent;
+}
+.tui-brown-outline::after {
+	border: 1px solid #ac9157 !important;
+}
+
+/*圆角 */
+
+.tui-fillet {
+	border-radius: 50rpx;
+}
+
+.tui-btn-white.tui-fillet::after {
+	border-radius: 98rpx;
+}
+
+.tui-outline-fillet::after {
+	border-radius: 98rpx;
+}
+
+/*平角*/
+.tui-rightAngle {
+	border-radius: 0;
+}
+
+.tui-btn-white.tui-rightAngle::after {
+	border-radius: 0;
+}
+
+.tui-outline-rightAngle::after {
+	border-radius: 0;
+}
+.tui-btn__link::after {
+	border: 0 !important;
+}
+</style>

+ 562 - 0
components/thorui/tui-calendar/tui-calendar.js

@@ -0,0 +1,562 @@
+/**
+ * @1900-2100区间内的公历、农历互转
+ * @公历转农历:solar2lunar(1987,11,01); 
+ * @农历转公历:lunar2solar(1987,09,10); 
+ */
+let calendar = {
+	/**
+	 * 农历1900-2100的润大小信息表
+	 * @Array Of Property
+	 * @return Hex
+	 */
+	lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, //1900-1909
+		0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, //1910-1919
+		0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, //1920-1929
+		0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, //1930-1939
+		0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, //1940-1949
+		0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, //1950-1959
+		0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, //1960-1969
+		0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, //1970-1979
+		0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, //1980-1989
+		0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, //1990-1999
+		0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, //2000-2009
+		0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, //2010-2019
+		0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, //2020-2029
+		0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, //2030-2039
+		0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, //2040-2049
+		0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, //2050-2059
+		0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, //2060-2069
+		0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, //2070-2079
+		0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, //2080-2089
+		0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, //2090-2099
+		0x0d520
+	], //2100
+	/**
+	 * 公历每个月份的天数普通表
+	 * @Array Of Property
+	 * @return Number
+	 */
+	solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+	/**
+	 * 天干地支之天干速查表
+	 * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
+	 * @return Cn string
+	 */
+	Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"],
+	/**
+	 * 天干地支之地支速查表
+	 * @Array Of Property
+	 * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
+	 * @return Cn string
+	 */
+	Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c",
+		"\u4ea5"
+	],
+	/**
+	 * 天干地支之地支速查表<=>生肖
+	 * @Array Of Property
+	 * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"]
+	 * @return Cn string
+	 */
+	Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21",
+		"\u72d7", "\u732a"
+	],
+	/**
+	 * 24节气速查表
+	 * @Array Of Property
+	 * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"]
+	 * @return Cn string
+	 */
+	solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206",
+		"\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691",
+		"\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d",
+		"\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"
+	],
+	/**
+	 * 1900-2100各年的24节气日期速查表
+	 * @Array Of Property
+	 * @return 0x string For splice
+	 */
+	sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
+		'97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+		'97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
+		'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
+		'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
+		'97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
+		'97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
+		'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
+		'97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+		'97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
+		'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
+		'97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+		'97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
+		'9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
+		'97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
+		'97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
+		'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
+		'7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+		'97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+		'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
+		'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+		'97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+		'9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
+		'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
+		'97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
+		'9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
+		'7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
+		'7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+		'9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+		'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
+		'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+		'97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+		'9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+		'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
+		'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
+		'977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
+		'7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+		'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
+		'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+		'977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+		'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+		'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
+		'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+		'977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+		'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
+		'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
+		'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
+		'7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+		'7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
+		'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
+		'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+		'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
+		'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
+		'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
+		'7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+		'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
+		'7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
+		'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
+		'665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+		'7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
+		'7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
+		'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'
+	],
+	/**
+	 * 数字转中文速查表
+	 * @Array Of Property
+	 * @trans ['日','一','二','三','四','五','六','七','八','九','十']
+	 * @return Cn string
+	 */
+	nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"],
+	/**
+	 * 日期转农历称呼速查表
+	 * @Array Of Property
+	 * @trans ['初','十','廿','卅']
+	 * @return Cn string
+	 */
+	nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"],
+	/**
+	 * 月份转农历称呼速查表
+	 * @Array Of Property
+	 * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊']
+	 * @return Cn string
+	 */
+	nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac",
+		"\u814a"
+	],
+	/**
+	 * 返回农历y年一整年的总天数
+	 * @param lunar Year
+	 * @return Number
+	 * @eg:let count = calendar.lYearDays(1987) ;//count=387
+	 */
+	lYearDays: function(y) {
+		let i, sum = 348;
+		for (i = 0x8000; i > 0x8; i >>= 1) {
+			sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0;
+		}
+		return (sum + calendar.leapDays(y));
+	},
+	/**
+	 * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0
+	 * @param lunar Year
+	 * @return Number (0-12)
+	 * @eg:let leapMonth = calendar.leapMonth(1987) ;//leapMonth=6
+	 */
+	leapMonth: function(y) { //闰字编码 \u95f0
+		return (calendar.lunarInfo[y - 1900] & 0xf);
+	},
+	/**
+	 * 返回农历y年闰月的天数 若该年没有闰月则返回0
+	 * @param lunar Year
+	 * @return Number (0、29、30)
+	 * @eg:let leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29
+	 */
+	leapDays: function(y) {
+		if (calendar.leapMonth(y)) {
+			return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
+		}
+		return (0);
+	},
+	/**
+	 * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法
+	 * @param lunar Year
+	 * @return Number (-1、29、30)
+	 * @eg:let MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29
+	 */
+	monthDays: function(y, m) {
+		if (m > 12 || m < 1) {
+			return -1
+		} //月份参数从1至12,参数错误返回-1
+		return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29);
+	},
+	/**
+	 * 返回公历(!)y年m月的天数
+	 * @param solar Year
+	 * @return Number (-1、28、29、30、31)
+	 * @eg:let solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30
+	 */
+	solarDays: function(y, m) {
+		if (m > 12 || m < 1) {
+			return -1
+		} //若参数错误 返回-1
+		let ms = m - 1;
+		if (ms == 1) { //2月份的闰平规律测算后确认返回28或29
+			return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28);
+		} else {
+			return (calendar.solarMonth[ms]);
+		}
+	},
+	/**
+	 * 农历年份转换为干支纪年
+	 * @param lYear 农历年的年份数
+	 * @return Cn string
+	 */
+	toGanZhiYear: function(lYear) {
+		let ganKey = (lYear - 3) % 10;
+		let zhiKey = (lYear - 3) % 12;
+		if (ganKey == 0) ganKey = 10; //如果余数为0则为最后一个天干
+		if (zhiKey == 0) zhiKey = 12; //如果余数为0则为最后一个地支
+		return calendar.Gan[ganKey - 1] + calendar.Zhi[zhiKey - 1];
+	},
+	/**
+	 * 公历月、日判断所属星座
+	 * @param cMonth [description]
+	 * @param cDay [description]
+	 * @return Cn string
+	 */
+	toAstro: function(cMonth, cDay) {
+		let s =
+			"\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf";
+		let arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
+		return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + "\u5ea7"; //座
+	},
+	/**
+	 * 传入offset偏移量返回干支
+	 * @param offset 相对甲子的偏移量
+	 * @return Cn string
+	 */
+	toGanZhi: function(offset) {
+		return calendar.Gan[offset % 10] + calendar.Zhi[offset % 12];
+	},
+	/**
+	 * 传入公历(!)y年获得该年第n个节气的公历日期
+	 * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起
+	 * @return day Number
+	 * @eg:let _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春
+	 */
+	getTerm: function(y, n) {
+		if (y < 1900 || y > 2100) {
+			return -1;
+		}
+		if (n < 1 || n > 24) {
+			return -1;
+		}
+		let _table = calendar.sTermInfo[y - 1900];
+		let _info = [
+			parseInt('0x' + _table.substr(0, 5)).toString(),
+			parseInt('0x' + _table.substr(5, 5)).toString(),
+			parseInt('0x' + _table.substr(10, 5)).toString(),
+			parseInt('0x' + _table.substr(15, 5)).toString(),
+			parseInt('0x' + _table.substr(20, 5)).toString(),
+			parseInt('0x' + _table.substr(25, 5)).toString()
+		];
+		let _calday = [
+			_info[0].substr(0, 1),
+			_info[0].substr(1, 2),
+			_info[0].substr(3, 1),
+			_info[0].substr(4, 2),
+			_info[1].substr(0, 1),
+			_info[1].substr(1, 2),
+			_info[1].substr(3, 1),
+			_info[1].substr(4, 2),
+			_info[2].substr(0, 1),
+			_info[2].substr(1, 2),
+			_info[2].substr(3, 1),
+			_info[2].substr(4, 2),
+			_info[3].substr(0, 1),
+			_info[3].substr(1, 2),
+			_info[3].substr(3, 1),
+			_info[3].substr(4, 2),
+			_info[4].substr(0, 1),
+			_info[4].substr(1, 2),
+			_info[4].substr(3, 1),
+			_info[4].substr(4, 2),
+			_info[5].substr(0, 1),
+			_info[5].substr(1, 2),
+			_info[5].substr(3, 1),
+			_info[5].substr(4, 2),
+		];
+		return parseInt(_calday[n - 1]);
+	},
+	/**
+	 * 传入农历数字月份返回汉语通俗表示法
+	 * @param lunar month
+	 * @return Cn string
+	 * @eg:let cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月'
+	 */
+	toChinaMonth: function(m) { // 月 => \u6708
+		if (m > 12 || m < 1) {
+			return -1
+		} //若参数错误 返回-1
+		let s = calendar.nStr3[m - 1];
+		s += "\u6708"; //加上月字
+		return s;
+	},
+	/**
+	 * 传入农历日期数字返回汉字表示法
+	 * @param lunar day
+	 * @return Cn string
+	 * @eg:let cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一'
+	 */
+	toChinaDay: function(d) { //日 => \u65e5
+		let s;
+		switch (d) {
+			case 10:
+				s = '\u521d\u5341';
+				break;
+			case 20:
+				s = '\u4e8c\u5341';
+				break;
+				break;
+			case 30:
+				s = '\u4e09\u5341';
+				break;
+				break;
+			default:
+				s = calendar.nStr2[Math.floor(d / 10)];
+				s += calendar.nStr1[d % 10];
+		}
+		return (s);
+	},
+	/**
+	 * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春”
+	 * @param y year
+	 * @return Cn string
+	 * @eg:let animal = calendar.getAnimal(1987) ;//animal='兔'
+	 */
+	getAnimal: function(y) {
+		return calendar.Animals[(y - 4) % 12]
+	},
+	/**
+	 * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON
+	 * @param y solar year
+	 * @param m solar month
+	 * @param d solar day
+	 * @return JSON object
+	 * @eg:console.log(calendar.solar2lunar(1987,11,01));
+	 */
+	solar2lunar: function(y, m, d) { //参数区间1900.1.31~2100.12.31
+		if (y < 1900 || y > 2100) {
+			return -1;
+		} //年份限定、上限
+		if (y == 1900 && m == 1 && d < 31) {
+			return -1;
+		} //下限
+		let objDate;
+		if (!y) { //未传参 获得当天
+			 objDate = new Date();
+		} else {
+			 objDate = new Date(y, parseInt(m) - 1, d)
+		}
+		let i, leap = 0,
+			temp = 0;
+		//修正ymd参数
+		y = objDate.getFullYear();
+		m = objDate.getMonth() + 1;
+		d = objDate.getDate();
+		let offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) /
+			86400000;
+		for (i = 1900; i < 2101 && offset > 0; i++) {
+			temp = calendar.lYearDays(i);
+			offset -= temp;
+		}
+		if (offset < 0) {
+			offset += temp;
+			i--;
+		}
+		//是否今天
+		let isTodayObj = new Date(),
+			isToday = false;
+		if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
+			isToday = true;
+		}
+		//星期几
+		let nWeek = objDate.getDay(),
+			cWeek = calendar.nStr1[nWeek];
+		if (nWeek == 0) {
+			nWeek = 7;
+		} //数字表示周几顺应天朝周一开始的惯例
+		//农历年
+		let year = i;
+		leap = calendar.leapMonth(i); //闰哪个月
+		let isLeap = false;
+		//效验闰月
+		for (i = 1; i < 13 && offset > 0; i++) {
+			//闰月
+			if (leap > 0 && i == (leap + 1) && isLeap == false) {
+				--i;
+				isLeap = true;
+				temp = calendar.leapDays(year); //计算农历闰月天数
+			} else {
+				temp = calendar.monthDays(year, i); //计算农历普通月天数
+			}
+			//解除闰月
+			if (isLeap == true && i == (leap + 1)) {
+				isLeap = false;
+			}
+			offset -= temp;
+		}
+		if (offset == 0 && leap > 0 && i == leap + 1)
+			if (isLeap) {
+				isLeap = false;
+			} else {
+				isLeap = true;
+				--i;
+			}
+		if (offset < 0) {
+			offset += temp;
+			--i;
+		}
+		//农历月
+		let month = i;
+		//农历日
+		let day = offset + 1;
+		//天干地支处理
+		let sm = m - 1;
+		let gzY = calendar.toGanZhiYear(year);
+		//月柱 1900年1月小寒以前为 丙子月(60进制12)
+		let firstNode = calendar.getTerm(year, (m * 2 - 1)); //返回当月「节」为几日开始
+		let secondNode = calendar.getTerm(year, (m * 2)); //返回当月「节」为几日开始
+		//依据12节气修正干支月
+		let gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11);
+		if (d >= firstNode) {
+			gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12);
+		}
+		//传入的日期的节气与否
+		let isTerm = false;
+		let Term = null;
+		if (firstNode == d) {
+			isTerm = true;
+			Term = calendar.solarTerm[m * 2 - 2];
+		}
+		if (secondNode == d) {
+			isTerm = true;
+			Term = calendar.solarTerm[m * 2 - 1];
+		}
+		//日柱 当月一日与 1900/1/1 相差天数
+		let dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
+		let gzD = calendar.toGanZhi(dayCyclical + d - 1);
+		//该日期所属的星座
+		let astro = calendar.toAstro(m, d);
+		return {
+			'lYear': year,
+			'lMonth': month,
+			'lDay': day,
+			'Animal': calendar.getAnimal(year),
+			'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month),
+			'IDayCn': calendar.toChinaDay(day),
+			'cYear': y,
+			'cMonth': m,
+			'cDay': d,
+			'gzYear': gzY,
+			'gzMonth': gzM,
+			'gzDay': gzD,
+			'isToday': isToday,
+			'isLeap': isLeap,
+			'nWeek': nWeek,
+			'ncWeek': "\u661f\u671f" + cWeek,
+			'isTerm': isTerm,
+			'Term': Term,
+			'astro': astro
+		};
+	},
+	/**
+	 * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON
+	 * @param y lunar year
+	 * @param m lunar month
+	 * @param d lunar day
+	 * @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
+	 * @return JSON object
+	 * @eg:console.log(calendar.lunar2solar(1987,9,10));
+	 */
+	lunar2solar: function(y, m, d, isLeapMonth) { //参数区间1900.1.31~2100.12.1
+		isLeapMonth = !!isLeapMonth;
+		let leapOffset = 0;
+		let leapMonth = calendar.leapMonth(y);
+		let leapDay = calendar.leapDays(y);
+		if (isLeapMonth && (leapMonth != m)) {
+			return -1;
+		} //传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
+		if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) {
+			return -1;
+		} //超出了最大极限值
+		let day = calendar.monthDays(y, m);
+		let _day = day;
+		//bugFix 2016-9-25
+		//if month is leap, _day use leapDays method
+		if (isLeapMonth) {
+			_day = calendar.leapDays(y, m);
+		}
+		if (y < 1900 || y > 2100 || d > _day) {
+			return -1;
+		} //参数合法性效验
+		//计算农历的时间差
+		let offset = 0;
+		for (let i = 1900; i < y; i++) {
+			offset += calendar.lYearDays(i);
+		}
+		let leap = 0,
+			isAdd = false;
+		for (let i = 1; i < m; i++) {
+			leap = calendar.leapMonth(y);
+			if (!isAdd) { //处理闰月
+				if (leap <= i && leap > 0) {
+					offset += calendar.leapDays(y);
+					isAdd = true;
+				}
+			}
+			offset += calendar.monthDays(y, i);
+		}
+		//转换闰月农历 需补充该年闰月的前一个月的时差
+		if (isLeapMonth) {
+			offset += day;
+		}
+		//1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
+		let stmap = Date.UTC(1900, 1, 30, 0, 0, 0);
+		let calObj = new Date((offset + d - 31) * 86400000 + stmap);
+		let cY = calObj.getUTCFullYear();
+		let cM = calObj.getUTCMonth() + 1;
+		let cD = calObj.getUTCDate();
+		return calendar.solar2lunar(cY, cM, cD);
+	}
+};
+
+module.exports = {
+	solar2lunar: calendar.solar2lunar,
+	lunar2solar: calendar.lunar2solar
+};

+ 864 - 0
components/thorui/tui-calendar/tui-calendar.vue

@@ -0,0 +1,864 @@
+<template>
+	<view>
+		<view :class="{ 'tui-bottom-popup': isFixed, 'tui-popup-show': isShow && isFixed }">
+			<view class="tui-calendar-header" :class="{ 'tui-calendar-radius': radius }" @touchmove.stop.prevent="stop" v-if="isFixed">
+				<view>日期选择</view>
+				<view class="tui-iconfont tui-font-close" hover-class="tui-opacity" :hover-stay-time="150" @tap="hide"></view>
+			</view>
+
+			<view class="tui-date-box">
+				<view
+					class="tui-iconfont tui-font-arrowleft"
+					:style="{ color: yearArrowColor }"
+					hover-class="tui-opacity"
+					:hover-stay-time="150"
+					v-if="arrowType == 1"
+					@tap="changeYear(0)"
+				></view>
+				<view class="tui-iconfont tui-font-arrowleft" :style="{ color: monthArrowColor }" hover-class="tui-opacity" :hover-stay-time="150" @tap="changeMonth(0)"></view>
+				<view class="tui-date_time">{{ showTitle }}</view>
+				<view class="tui-iconfont tui-font-arrowright" :style="{ color: monthArrowColor }" hover-class="tui-opacity" :hover-stay-time="150" @tap="changeMonth(1)"></view>
+				<view
+					class="tui-iconfont tui-font-arrowright"
+					:style="{ color: yearArrowColor }"
+					hover-class="tui-opacity"
+					:hover-stay-time="150"
+					v-if="arrowType == 1"
+					@tap="changeYear(1)"
+				></view>
+			</view>
+			<view class="tui-date-header">
+				<view class="tui-date">日</view>
+				<view class="tui-date">一</view>
+				<view class="tui-date">二</view>
+				<view class="tui-date">三</view>
+				<view class="tui-date">四</view>
+				<view class="tui-date">五</view>
+				<view class="tui-date">六</view>
+			</view>
+			<view class="tui-date-content" :class="{ 'tui-flex-start': isFixed && fixedHeight }" :style="{ height: isFixed && fixedHeight ? dateHeight * 6 + 'px' : 'auto' }">
+				<block v-for="(item, index) in weekdayArr" :key="index"><view class="tui-date"></view></block>
+				<view
+					class="tui-date"
+					:class="{
+						'tui-date-pd_0': isFixed && fixedHeight,
+						'tui-opacity': openDisAbled(year, month, index + 1),
+						'tui-start-date': (type == 2 && startDate == `${year}-${month}-${index + 1}`) || type == 1,
+						'tui-end-date': (type == 2 && endDate == `${year}-${month}-${index + 1}`) || type == 1
+					}"
+					:style="{ backgroundColor: isFixed ? getColor(index, 1) : 'transparent', height: isFixed && fixedHeight ? dateHeight + 'px' : 'auto' }"
+					v-for="(item, index) in daysArr"
+					:key="index"
+					@tap="dateClick(index)"
+				>
+					<view class="tui-date-text" :style="{ color: isFixed ? getColor(index, 2) : getStatusData(3, index), backgroundColor: getStatusData(2, index) }">
+						<view v-if="isFixed || !getStatusData(4, index)">{{ index + 1 }}</view>
+						<view v-if="!getStatusData(4, index)" class="tui-custom-desc" :class="{ 'tui-lunar-unshow': !lunar && isFixed }">
+							{{ getDescText(index, startDate, endDate) }}
+						</view>
+						<text class="tui-iconfont tui-font-check" v-if="getStatusData(4, index)"></text>
+					</view>
+					<view class="tui-date-desc" :style="{ color: activeColor }" v-if="!lunar && type == 2 && startDate == `${year}-${month}-${index + 1}` && startDate != endDate">
+						{{ startText }}
+					</view>
+					<view class="tui-date-desc" :style="{ color: activeColor }" v-if="!lunar && type == 2 && endDate == `${year}-${month}-${index + 1}`">{{ endText }}</view>
+				</view>
+				<view class="tui-bg-month">{{ month }}</view>
+			</view>
+
+			<view class="tui-calendar-op" v-if="isFixed" @touchmove.stop.prevent="stop">
+				<view class="tui-calendar-result">
+					<text>{{ type == 1 ? activeDate : startDate }}</text>
+					<text v-if="endDate">至{{ endDate }}</text>
+				</view>
+				<view class="tui-calendar-btn_box"><tui-button :type="btnType" height="72rpx" shape="circle" :size="28" @click="btnFix(false)">确定</tui-button></view>
+			</view>
+		</view>
+
+		<view class="tui-popup-mask" :class="[isShow ? 'tui-mask-show' : '']" @touchmove.stop.prevent="stop" v-if="isFixed" @tap="hide"></view>
+	</view>
+</template>
+<script>
+//easycom组件模式 无需手动引入
+// import tuiButton from "../tui-button/tui-button"
+const calendar = require('./tui-calendar.js');
+export default {
+	name: 'tuiCalendar',
+	// components:{
+	// 	tuiButton
+	// },
+	props: {
+		//1-切换月份和年份 2-切换月份
+		arrowType: {
+			type: [Number, String],
+			default: 1
+		},
+		//1-单个日期选择 2-开始日期+结束日期选择
+		type: {
+			type: Number,
+			default: 1
+		},
+		//可切换最大年份
+		maxYear: {
+			type: Number,
+			default: 2030
+		},
+		//可切换最小年份
+		minYear: {
+			type: Number,
+			default: 1920
+		},
+		//最小可选日期(不在范围内日期禁用不可选)
+		minDate: {
+			type: String,
+			default: '1920-01-01'
+		},
+		/**
+		 * 最大可选日期
+		 * 默认最大值为今天,之后的日期不可选
+		 * 2030-12-31
+		 * */
+		maxDate: {
+			type: String,
+			default: ''
+		},
+		//显示圆角
+		radius: {
+			type: Boolean,
+			default: true
+		},
+		//状态 数据顺序与当月天数一致,index=>day
+		/**
+				 * [{
+					 * text:"", 描述:2字以内
+					 * value:"",状态值 
+					 * bgColor:"",背景色
+					 * color:""  文字颜色,
+					 * check:false //是否显示对勾
+					 * 
+				 }]
+				 * 
+				 * **/
+		status: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		//月份切换箭头颜色
+		monthArrowColor: {
+			type: String,
+			default: '#999'
+		},
+		//年份切换箭头颜色
+		yearArrowColor: {
+			type: String,
+			default: '#bcbcbc'
+		},
+		//默认日期字体颜色
+		color: {
+			type: String,
+			default: '#333'
+		},
+		//选中|起始结束日期背景色
+		activeBgColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//选中|起始结束日期字体颜色
+		activeColor: {
+			type: String,
+			default: '#fff'
+		},
+		//范围内日期背景色
+		rangeBgColor: {
+			type: String,
+			default: 'rgba(86,119,252,0.1)'
+		},
+		//范围内日期字体颜色
+		rangeColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//type=2时生效,起始日期自定义文案
+		startText: {
+			type: String,
+			default: '开始'
+		},
+		//type=2时生效,结束日期自定义文案
+		endText: {
+			type: String,
+			default: '结束'
+		},
+		//按钮样式类型
+		btnType: {
+			type: String,
+			default: 'primary'
+		},
+		//固定在底部
+		isFixed: {
+			type: Boolean,
+			default: false
+		},
+		//固定日历容器高度,isFixed=true时生效
+		fixedHeight: {
+			type: Boolean,
+			default: true
+		},
+		//当前选中日期带选中效果
+		isActiveCurrent: {
+			type: Boolean,
+			default: true
+		},
+		//切换年月是否触发事件 type=1时生效
+		isChange: {
+			type: Boolean,
+			default: false
+		},
+		//是否显示农历
+		lunar: {
+			type: Boolean,
+			default: false
+		},
+		//初始化起始选中日期 格式: 2020-06-06 或 2020/06/06 【type=1 or 2】
+		initStartDate: {
+			type: String,
+			default: ''
+		},
+		//初始化结束日期 格式: 2020-06-06 或 2020/06/06【type=2】
+		initEndDate: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {
+			isShow: false,
+			weekday: 1, // 星期几,值为1-7
+			weekdayArr: [],
+			days: 0, //当前月有多少天
+			daysArr: [],
+			showTitle: '',
+			year: 2020,
+			month: 0,
+			day: 0,
+			startYear: 0,
+			startMonth: 0,
+			startDay: 0,
+			endYear: 0,
+			endMonth: 0,
+			endDay: 0,
+			today: '',
+			activeDate: '',
+			startDate: '',
+			endDate: '',
+			isStart: true,
+			min: null,
+			max: null,
+			dateHeight: 20
+		};
+	},
+	computed: {
+		dataChange() {
+			return `${this.type}-${this.minDate}-${this.maxDate}-${this.initStartDate}-${this.initEndDate}`;
+		}
+	},
+	watch: {
+		dataChange(val) {
+			this.init();
+		},
+		fixedHeight(val) {
+			if (val) {
+				this.initDateHeight();
+			}
+		}
+	},
+	created() {
+		this.init();
+	},
+	methods: {
+		getColor(index, type) {
+			let color = type == 1 ? '' : this.color;
+			let day = index + 1;
+			let date = `${this.year}-${this.month}-${day}`;
+			let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+			let start = this.startDate.replace(/\-/g, '/');
+			let end = this.endDate.replace(/\-/g, '/');
+			if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+				color = type == 1 ? this.activeBgColor : this.activeColor;
+			} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+				color = type == 1 ? this.rangeBgColor : this.rangeColor;
+			}
+			return color;
+		},
+		//获取状态数据
+		getStatusData(type, index) {
+			//1-描述text,2-bgColor背景色,3-color文字颜色 4-check 是否显示对勾
+			let val = ['', 'transparent', '#333', ''][type - 1];
+			if (!this.isFixed && this.status && this.status.length > 0) {
+				let item = this.status[index];
+				if (item) {
+					switch (type) {
+						case 1:
+							val = item.text;
+							break;
+						case 2:
+							val = item.bgColor;
+							break;
+						case 3:
+							val = item.color;
+							break;
+						case 4:
+							val = item.check;
+							break;
+						default:
+							break;
+					}
+				}
+			}
+			return val;
+		},
+		getDescText(index, startDate, endDate) {
+			let text = this.lunar ? this.getLunar(this.year, this.month, index + 1) : '';
+			if (this.isFixed && this.type == 2) {
+				//此判断不能与上面条件一起判断
+				if (this.lunar) {
+					let date = `${this.year}-${this.month}-${index + 1}`;
+					if (startDate == date && startDate != endDate) {
+						text = this.startText;
+					} else if (endDate == date) {
+						text = this.endText;
+					}
+				}
+			} else {
+				let status = this.getStatusData(1, index);
+				if (status) text = status;
+			}
+			return text;
+		},
+		getLunar(year, month, day) {
+			let obj = calendar.solar2lunar(year, month, day);
+			return obj.IDayCn;
+		},
+		initDateHeight() {
+			if (this.fixedHeight && this.isFixed) {
+				this.dateHeight = uni.getSystemInfoSync().windowWidth / 7;
+			}
+		},
+		init() {
+			this.initDateHeight();
+			let now = new Date();
+			this.year = now.getFullYear();
+			this.month = now.getMonth() + 1;
+			this.day = now.getDate();
+			this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+			this.activeDate = this.today;
+			this.min = this.initDate(this.minDate);
+			this.max = this.initDate(this.maxDate || this.today);
+			this.startDate = '';
+			this.startYear = 0;
+			this.startMonth = 0;
+			this.startDay = 0;
+			if (this.initStartDate) {
+				let start = new Date(this.initStartDate.replace(/\-/g, '/'));
+				if (this.type == 1) {
+					this.year = start.getFullYear();
+					this.month = start.getMonth() + 1;
+					this.day = start.getDate();
+					this.activeDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
+				} else {
+					this.startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
+					this.startYear = start.getFullYear();
+					this.startMonth = start.getMonth() + 1;
+					this.startDay = start.getDate();
+					this.activeDate = '';
+				}
+				
+			}
+			this.endYear = 0;
+			this.endMonth = 0;
+			this.endDay = 0;
+			this.endDate = '';
+			if (this.initEndDate && this.type == 2) {
+				let end = new Date(this.initEndDate.replace(/\-/g, '/'));
+				this.endDate = `${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`;
+				this.endYear = end.getFullYear();
+				this.endMonth = end.getMonth() + 1;
+				this.endDay = end.getDate();
+				this.activeDate = '';
+				this.year = end.getFullYear();
+				this.month = end.getMonth() + 1;
+				this.day = end.getDate();
+			}
+			this.isStart = true;
+			this.changeData();
+		},
+		//日期处理
+		initDate(date) {
+			let fdate = date.split('-');
+			return {
+				year: Number(fdate[0] || 1920),
+				month: Number(fdate[1] || 1),
+				day: Number(fdate[2] || 1)
+			};
+		},
+		openDisAbled: function(year, month, day) {
+			let bool = true;
+			let date = `${year}/${month}/${day}`;
+			// let today = this.today.replace(/\-/g, '/');
+			let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+			let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+			let timestamp = new Date(date).getTime();
+			if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+				bool = false;
+			}
+			return bool;
+		},
+		generateArray: function(start, end) {
+			return Array.from(new Array(end + 1).keys()).slice(start);
+		},
+		formatNum: function(num) {
+			return num < 10 ? '0' + num : num + '';
+		},
+		stop() {
+			return !this.isFixed;
+		},
+		//一个月有多少天
+		getMonthDay(year, month) {
+			let days = new Date(year, month, 0).getDate();
+			return days;
+		},
+		getWeekday(year, month) {
+			let date = new Date(`${year}/${month}/01 00:00:00`);
+			return date.getDay();
+		},
+		checkRange(year) {
+			let overstep = false;
+			if (year < this.minYear || year > this.maxYear) {
+				uni.showToast({
+					title: '日期超出范围啦~',
+					icon: 'none'
+				});
+				overstep = true;
+			}
+			return overstep;
+		},
+		changeMonth(isAdd) {
+			if (isAdd) {
+				let month = this.month + 1;
+				let year = month > 12 ? this.year + 1 : this.year;
+				if (!this.checkRange(year)) {
+					this.month = month > 12 ? 1 : month;
+					this.year = year;
+					this.changeData();
+				}
+			} else {
+				let month = this.month - 1;
+				let year = month < 1 ? this.year - 1 : this.year;
+				if (!this.checkRange(year)) {
+					this.month = month < 1 ? 12 : month;
+					this.year = year;
+					this.changeData();
+				}
+			}
+		},
+		changeYear(isAdd) {
+			let year = isAdd ? this.year + 1 : this.year - 1;
+			if (!this.checkRange(year)) {
+				this.year = year;
+				this.changeData();
+			}
+		},
+		changeData() {
+			this.days = this.getMonthDay(this.year, this.month);
+			this.daysArr = this.generateArray(1, this.days);
+			this.weekday = this.getWeekday(this.year, this.month);
+			this.weekdayArr = this.generateArray(1, this.weekday);
+			this.showTitle = `${this.year}年${this.month}月`;
+			if (this.isChange && this.type == 1) {
+				this.btnFix(true);
+			}
+		},
+		dateClick: function(day) {
+			day += 1;
+			if (!this.openDisAbled(this.year, this.month, day)) {
+				this.day = day;
+				let date = `${this.year}-${this.month}-${day}`;
+				if (this.type == 1) {
+					this.activeDate = date;
+				} else {
+					let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime();
+					if (this.isStart || compare) {
+						this.startDate = date;
+						this.startYear = this.year;
+						this.startMonth = this.month;
+						this.startDay = this.day;
+						this.endYear = 0;
+						this.endMonth = 0;
+						this.endDay = 0;
+						this.endDate = '';
+						this.activeDate = '';
+						this.isStart = false;
+					} else {
+						this.endDate = date;
+						this.endYear = this.year;
+						this.endMonth = this.month;
+						this.endDay = this.day;
+						this.isStart = true;
+					}
+				}
+				if (!this.isFixed) {
+					this.btnFix();
+				}
+			}
+		},
+		show() {
+			this.isShow = true;
+		},
+		hide() {
+			this.isShow = false;
+			this.$emit('hide', {})
+		},
+		getWeekText(date) {
+			date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+			let week = date.getDay();
+			return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+		},
+		btnFix(show) {
+			if (!show) {
+				this.hide();
+			}
+			if (this.type == 1) {
+				let arr = this.activeDate.split('-');
+				let year = this.isChange ? this.year : Number(arr[0]);
+				let month = this.isChange ? this.month : Number(arr[1]);
+				let day = this.isChange ? this.day : Number(arr[2]);
+				//当前月有多少天
+				let days = this.getMonthDay(year, month);
+				let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+				let weekText = this.getWeekText(result);
+				let isToday = false;
+				if (`${year}-${month}-${day}` == this.today) {
+					//今天
+					isToday = true;
+				}
+				let lunar = calendar.solar2lunar(year, month, day);
+				this.$emit('change', {
+					year: year,
+					month: month,
+					day: day,
+					days: days,
+					result: result,
+					week: weekText,
+					isToday: isToday,
+					switch: show, //是否是切换年月操作
+					lunar: lunar
+				});
+			} else {
+				if (!this.startDate || !this.endDate) return;
+				let startMonth = this.formatNum(this.startMonth);
+				let startDay = this.formatNum(this.startDay);
+				let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+				let startWeek = this.getWeekText(startDate);
+				let startLunar = calendar.solar2lunar(this.startYear, startMonth, startDay);
+
+				let endMonth = this.formatNum(this.endMonth);
+				let endDay = this.formatNum(this.endDay);
+				let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+				let endWeek = this.getWeekText(endDate);
+				let endLunar = calendar.solar2lunar(this.endYear, endMonth, endDay);
+				this.$emit('change', {
+					startYear: this.startYear,
+					startMonth: this.startMonth,
+					startDay: this.startDay,
+					startDate: startDate,
+					startWeek: startWeek,
+					startLunar: startLunar,
+					endYear: this.endYear,
+					endMonth: this.endMonth,
+					endDay: this.endDay,
+					endDate: endDate,
+					endWeek: endWeek,
+					endLunar: endLunar
+				});
+			}
+		}
+	}
+};
+</script>
+
+<style scoped>
+@font-face {
+	font-family: 'tuiDateFont';
+	src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAVgAA0AAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAFRAAAABoAAAAci0/w50dERUYAAAUkAAAAHgAAAB4AKQANT1MvMgAAAaAAAABDAAAAVjxuSNNjbWFwAAAB+AAAAEoAAAFS5iPQt2dhc3AAAAUcAAAACAAAAAj//wADZ2x5ZgAAAlQAAAFHAAABvPf29TBoZWFkAAABMAAAADAAAAA2GMsN3WhoZWEAAAFgAAAAHQAAACQHjAOFaG10eAAAAeQAAAATAAAAFgzQAPJsb2NhAAACRAAAABAAAAAQAOoBSG1heHAAAAGAAAAAHgAAACABEwA3bmFtZQAAA5wAAAFJAAACiCnmEVVwb3N0AAAE6AAAADQAAABLUwjqHHjaY2BkYGAAYp5Gj5/x/DZfGbhZGEDg1tUn7+F00P/LzOuY9YFcDgYmkCgAa0gNlHjaY2BkYGBu+N/AEMPCAALM6xgYGVABCwBT4AMaAAAAeNpjYGRgYGBn0GZgYgABEMkFhAwM/8F8BgANaAFLAAB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PGJ49ZG7438AQw9zA0AAUZgTJAQDrcAy8AHjaY2GAABYIDgLCBQx1AAcEAc8AeNpjYGBgZoBgGQZGBhDwAfIYwXwWBgMgzQGETAwMzxifcTx7+P8/kMUAYUkxS/6VVIXqAgNGNgY4lxGoB6QPBTAyDHsAADDkDYkAAAAAAAAAAAAAADQAagC2AN542m2QsU7DMBCG/Tt1bNPUiUnkSgiVtqKpxJAgVLVbeAa6MaK+B4JXgJWBjY21UtW5gpkdMTFX7dzApaJLhXU6n8+n//ttxtn458N79XJWZ8eMxS00C4wy9A1EP8PQncAlIQzS4WgsVtPpSmwzV3OFRqLetH5TSQMK939X61ptPZ2p2EAttNMLBRMrtschQblDeS34aY50cIkCzg/B2Y5C+VpyQxhFkRgu515O8jvU5mmPM2O0wJ5Z27vhX+yMsV437WvCdTM+GI40MgwKfuGammC0uURqeqFMfe9cxaJclkt5GMaB1hIR1VobOgpEiKq+sLZcIrJWhO3/Jw7qWlYj1Jf21FaCtmd5bevrlk28O/7A4spXTl4KTh9MTlqQ8PESBRstReic+sRj0Dni9fIqmNS/pXNWCvWOeYBmx5S9Bsn9Ah+5WtAAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAO2MGiTIxMjMyMLIys7GmJeRmlmWZQ2pQ5OSORLaU0Mz2/FACDfwlbAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABgABAAQAAAACAAAAAHjaY2BgYGQAgqtL1DlA9K2rT97DaABNlwiuAAA=)
+		format('woff');
+	font-weight: normal;
+	font-style: normal;
+}
+
+.tui-iconfont {
+	font-family: 'tuiDateFont' !important;
+	font-size: 36rpx;
+	font-style: normal;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+}
+
+.tui-font-close:before {
+	content: '\e608';
+}
+
+.tui-font-check:before {
+	content: '\e6e1';
+}
+
+.tui-font-arrowright:before {
+	content: '\e600';
+}
+
+.tui-font-arrowleft:before {
+	content: '\e601';
+}
+
+.tui-date-box {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 20rpx 0 30rpx;
+	background-color: #fff;
+}
+
+.tui-calendar-radius {
+	border-top-left-radius: 20rpx;
+	border-top-right-radius: 20rpx;
+	overflow: hidden;
+}
+
+.tui-date_time {
+	padding: 0 16rpx;
+	color: #333;
+	font-size: 32rpx;
+	line-height: 32rpx;
+	font-weight: bold;
+}
+
+.tui-font-arrowleft {
+	margin-right: 32rpx;
+}
+
+.tui-font-arrowright {
+	margin-left: 32rpx;
+}
+
+.tui-date-header {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	background-color: #fff;
+	font-size: 24rpx;
+	line-height: 24rpx;
+	color: #555;
+	box-shadow: 0 15rpx 20rpx -15rpx #efefef;
+	position: relative;
+	z-index: 2;
+}
+
+.tui-date-content {
+	width: 100%;
+	display: flex;
+	flex-wrap: wrap;
+	padding: 12rpx 0;
+	box-sizing: border-box;
+	background-color: #fff;
+	position: relative;
+}
+
+.tui-flex-start {
+	align-content: flex-start;
+}
+
+.tui-bg-month {
+	position: absolute;
+	font-size: 260rpx;
+	line-height: 260rpx;
+	left: 50%;
+	top: 50%;
+	transform: translate(-50%, -50%);
+	color: #f5f5f7;
+	z-index: 1;
+}
+
+.tui-date {
+	width: 14.2857%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 12rpx 0;
+	overflow: hidden;
+	position: relative;
+	z-index: 2;
+}
+
+.tui-date-pd_0 {
+	padding: 0 !important;
+}
+
+.tui-start-date {
+	border-top-left-radius: 8rpx;
+	border-bottom-left-radius: 8rpx;
+}
+
+.tui-end-date {
+	border-top-right-radius: 8rpx;
+	border-bottom-right-radius: 8rpx;
+}
+
+.tui-date-text {
+	width: 80rpx;
+	height: 80rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	font-size: 32rpx;
+	line-height: 32rpx;
+	position: relative;
+	border-radius: 50%;
+}
+
+.tui-btn-calendar {
+	padding: 16rpx;
+	box-sizing: border-box;
+	text-align: center;
+	text-decoration: none;
+}
+
+.tui-opacity {
+	opacity: 0.5;
+}
+
+.tui-bottom-popup {
+	width: 100%;
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 9999;
+	visibility: hidden;
+	transform: translate3d(0, 100%, 0);
+	transform-origin: center;
+	transition: all 0.3s ease-in-out;
+	min-height: 20rpx;
+}
+
+.tui-popup-show {
+	transform: translate3d(0, 0, 0);
+	visibility: visible;
+}
+
+.tui-popup-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.6);
+	z-index: 9996;
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	visibility: hidden;
+}
+
+.tui-mask-show {
+	opacity: 1;
+	visibility: visible;
+}
+
+.tui-calendar-header {
+	width: 100%;
+	height: 80rpx;
+	padding: 0 40rpx;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	box-sizing: border-box;
+	font-size: 30rpx;
+	background-color: #fff;
+	color: #555;
+	position: relative;
+}
+
+.tui-font-close {
+	position: absolute;
+	right: 30rpx;
+	top: 50%;
+	transform: translateY(-50%);
+	color: #999;
+}
+
+.tui-btn-calendar {
+	padding: 16rpx;
+	box-sizing: border-box;
+	text-align: center;
+	text-decoration: none;
+}
+
+.tui-font-check {
+	color: #fff;
+	font-size: 54rpx;
+	line-height: 54rpx;
+}
+
+.tui-custom-desc {
+	width: 100%;
+	font-size: 24rpx;
+	line-height: 24rpx;
+	transform: scale(0.8);
+	transform-origin: center center;
+	text-align: center;
+}
+
+.tui-lunar-unshow {
+	position: absolute;
+	left: 0;
+	bottom: 8rpx;
+	z-index: 2;
+}
+
+.tui-date-desc {
+	width: 100%;
+	font-size: 24rpx;
+	line-height: 24rpx;
+	position: absolute;
+	left: 0;
+	transform: scale(0.8);
+	transform-origin: center center;
+	text-align: center;
+	bottom: 8rpx;
+	z-index: 2;
+}
+
+.tui-calendar-op {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	background-color: #fff;
+	padding: 0 42rpx 30rpx;
+	box-sizing: border-box;
+	font-size: 24rpx;
+	color: #666;
+}
+
+.tui-calendar-result {
+	height: 48rpx;
+	transform: scale(0.9);
+	transform-origin: center 100%;
+}
+
+.tui-calendar-btn_box {
+	width: 100%;
+}
+</style>

+ 211 - 0
components/thorui/tui-card/tui-card.vue

@@ -0,0 +1,211 @@
+<template>
+	<view class="tui-card-class tui-card" :class="[full?'tui-card-full':'',border?'tui-card-border':'']" @tap="handleClick"
+	 @longtap="longTap">
+		<view class="tui-card-header" :class="{'tui-header-line':header.line}" :style="{background:header.bgcolor || '#fff'}">
+			<view class="tui-header-left">
+				<image :src="image.url" class="tui-header-thumb" :class="{'tui-thumb-circle':image.circle}" mode="widthFix" v-if="image.url"
+				 :style="{height:(image.height || 60)+'rpx',width:(image.width || 60)+'rpx'}"></image>
+				<text class="tui-header-title" :style="{fontSize:(title.size || 30)+'rpx',color:(title.color || '#7A7A7A')}" v-if="title.text">{{title.text}}</text>
+			</view>
+			<view class="tui-header-right" :style="{fontSize:(tag.size || 24)+'rpx',color:(tag.color || '#b2b2b2')}" v-if="tag.text">
+				{{tag.text}}
+			</view>
+		</view>
+		<view class="tui-card-body">
+			<slot name="body"></slot>
+		</view>
+		<view class="tui-card-footer">
+			<slot name="footer"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiCard",
+		props: {
+			//是否铺满
+			full: {
+				type: Boolean,
+				default: false
+			},
+			image: {
+				type: Object,
+				default: function() {
+					return {
+						url: "", //图片地址
+						height: 60, //图片高度
+						width: 60, //图片宽度
+						circle: false
+					}
+				}
+			},
+			//标题
+			title: {
+				type: Object,
+				default: function() {
+					return {
+						text: "", //标题文字
+						size: 30, //字体大小
+						color: "#7A7A7A" //字体颜色
+					}
+				}
+			},
+			//标签,时间等
+			tag: {
+				type: Object,
+				default: function() {
+					return {
+						text: "", //标签文字
+						size: 24, //字体大小
+						color: "#b2b2b2" //字体颜色
+					}
+				}
+			},
+			header: {
+				type: Object,
+				default: function() {
+					return {
+						bgcolor: "#fff", //背景颜色
+						line: false //是否去掉底部线条
+					}
+				}
+			},
+			//是否设置外边框
+			border: {
+				type: Boolean,
+				default: false
+			},
+			index: {
+				type: Number,
+				default: 0
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {
+					index: this.index
+				});
+			},
+			longTap() {
+				this.$emit('longclick', {
+					index: this.index
+				});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-card {
+		margin: 0 30rpx;
+		font-size: 28rpx;
+		background-color: #fff;
+		border-radius: 10rpx;
+		box-shadow: 0 0 10rpx #eee;
+		box-sizing: border-box;
+		overflow: hidden;
+	}
+
+	.tui-card-full {
+		margin: 0 !important;
+		border-radius: 0 !important;
+	}
+
+	.tui-card-full::after {
+		border-radius: 0 !important;
+	}
+
+	.tui-card-border {
+		position: relative;
+		box-shadow: none !important
+	}
+
+	.tui-card-border::after {
+		content: ' ';
+		position: absolute;
+		height: 200%;
+		width: 200%;
+		border: 1px solid #ddd;
+		transform-origin: 0 0;
+		-webkit-transform-origin: 0 0;
+		-webkit-transform: scale(0.5);
+		transform: scale(0.5);
+		left: 0;
+		top: 0;
+		border-radius: 20rpx;
+		box-sizing: border-box;
+		pointer-events: none;
+	}
+
+	.tui-card-header {
+		width: 100%;
+		padding: 20rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		position: relative;
+		box-sizing: border-box;
+		overflow: hidden;
+		border-top-left-radius: 10rpx;
+		border-top-right-radius: 10rpx;
+	}
+
+	.tui-card-header::after {
+		content: '';
+		position: absolute;
+		border-bottom: 1rpx solid #eaeef1;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		bottom: 0;
+		right: 0;
+		left: 0;
+		pointer-events: none;
+	}
+
+	.tui-header-line::after {
+		border-bottom: 0 !important;
+	}
+
+	.tui-header-thumb {
+		height: 60rpx;
+		width: 60rpx;
+		vertical-align: middle;
+		margin-right: 20rpx;
+		border-radius: 6rpx;
+	}
+
+	.tui-thumb-circle {
+		border-radius: 50% !important;
+	}
+
+	.tui-header-title {
+		display: inline-block;
+		font-size: 30rpx;
+		color: #7a7a7a;
+		vertical-align: middle;
+		max-width: 460rpx;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
+
+	.tui-header-right {
+		font-size: 24rpx;
+		color: #b2b2b2;
+	}
+
+	.tui-card-body {
+		font-size: 32rpx;
+		color: #262b3a;
+		box-sizing: border-box;
+	}
+
+	.tui-card-footer {
+		font-size: 28rpx;
+		color: #596d96;
+		border-bottom-left-radius: 10rpx;
+		border-bottom-right-radius: 10rpx;
+		box-sizing: border-box;
+	}
+</style>

+ 521 - 0
components/thorui/tui-cascade-selection/tui-cascade-selection.vue

@@ -0,0 +1,521 @@
+<template>
+	<view class="tui-cascade-selection">
+		<scroll-view
+			scroll-x
+			scroll-with-animation
+			:scroll-into-view="scrollViewId"
+			:style="{ backgroundColor: headerBgColor }"
+			class="tui-bottom-line"
+			:class="{ 'tui-btm-none': !headerLine }"
+		>
+			<view class="tui-selection-header" :style="{ height: tabsHeight, backgroundColor: backgroundColor }">
+				<view
+					class="tui-header-item"
+					:class="{ 'tui-font-bold': index === currentTab && bold }"
+					:style="{ color: index === currentTab ? activeColor : color, fontSize: size + 'rpx' }"
+					:id="`id_${index}`"
+					@tap.stop="swichNav"
+					:data-current="index"
+					v-for="(item, index) in selectedArr"
+					:key="index"
+				>
+					{{ item.text }}
+					<view class="tui-active-line" :style="{ backgroundColor: lineColor }" v-if="index === currentTab && showLine"></view>
+				</view>
+			</view>
+		</scroll-view>
+		<swiper class="tui-selection-list" :current="currentTab" duration="300" @change="switchTab" :style="{ height: height, backgroundColor: backgroundColor }">
+			<swiper-item v-for="(item, index) in selectedArr" :key="index">
+				<scroll-view scroll-y :scroll-into-view="item.scrollViewId" class="tui-selection-item" :style="{ height: height }">
+					<view class="tui-first-item" :style="{ height: firstItemTop }"></view>
+					<view
+						class="tui-selection-cell"
+						:style="{ padding: padding, backgroundColor: backgroundColor }"
+						:id="`id_${subIndex}`"
+						v-for="(subItem, subIndex) in item.list"
+						:key="subIndex"
+						@tap="change(index, subIndex, subItem)"
+					>
+						<icon type="success_no_circle" v-if="item.index === subIndex" :color="checkMarkColor" :size="checkMarkSize" class="tui-icon-success"></icon>
+						<image :src="subItem.src" v-if="subItem.src" class="tui-cell-img" :style="{ width: imgWidth, height: imgHeight, borderRadius: radius }"></image>
+						<view
+							class="tui-cell-title"
+							:class="{ 'tui-font-bold': item.index === subIndex && textBold, 'tui-flex-shrink': nowrap }"
+							:style="{ color: item.index === subIndex ? textActiveColor : textColor, fontSize: textSize + 'rpx' }"
+						>
+							{{ subItem.text }}
+						</view>
+						<view class="tui-cell-sub_title" :style="{ color: subTextColor, fontSize: subTextSize + 'rpx' }" v-if="subItem.subText">{{ subItem.subText }}</view>
+					</view>
+				</scroll-view>
+			</swiper-item>
+		</swiper>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiCascadeSelection',
+	props: {
+		/**
+			 * 如果下一级是请求返回,则为第一级数据,否则所有数据
+			 * 数据格式
+			  [{
+				  src: "",
+				  text: "",
+				  subText: "",
+				  value: 0,
+				  children:[{
+					  text: "",
+					  subText: "",
+					  value: 0,
+					  children:[]
+			   }]
+			  }]
+			 * */
+		itemList: {
+			type: Array,
+			default: () => {
+				return [];
+			}
+		},
+		/*
+		   初始化默认选中数据
+		   [{
+			text: "",//选中text
+			subText: '',//选中subText
+			value: '',//选中value
+			src: '', //选中src,没有则传空或不传
+			index: 0, //选中数据在当前layer索引
+			list: [{src: "", text: "", subText: "", value: 101}] //所有layer数据集合
+		  }];
+		    
+		   */
+		defaultItemList: {
+			type: Array,
+			value: []
+		},
+		//是否显示header底部细线
+		headerLine: {
+			type: Boolean,
+			default: true
+		},
+		//header背景颜色
+		headerBgColor: {
+			type: String,
+			default: '#FFFFFF'
+		},
+		//顶部标签栏高度
+		tabsHeight: {
+			type: String,
+			default: '88rpx'
+		},
+		//默认显示文字
+		text: {
+			type: String,
+			default: '请选择'
+		},
+		//tabs 文字大小
+		size: {
+			type: Number,
+			default: 28
+		},
+		//tabs 文字颜色
+		color: {
+			type: String,
+			default: '#555'
+		},
+		//选中颜色
+		activeColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//选中后文字加粗
+		bold: {
+			type: Boolean,
+			default: true
+		},
+		//选中后是否显示底部线条
+		showLine: {
+			type: Boolean,
+			default: true
+		},
+		//线条颜色
+		lineColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//icon 大小
+		checkMarkSize: {
+			type: Number,
+			default: 15
+		},
+		//icon 颜色
+		checkMarkColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//item 图片宽度
+		imgWidth: {
+			type: String,
+			default: '40rpx'
+		},
+		//item 图片高度
+		imgHeight: {
+			type: String,
+			default: '40rpx'
+		},
+		//图片圆角
+		radius: {
+			type: String,
+			default: '50%'
+		},
+		//item text颜色
+		textColor: {
+			type: String,
+			default: '#333'
+		},
+		textActiveColor: {
+			type: String,
+			default: '#333'
+		},
+		//选中后字体是否加粗
+		textBold: {
+			type: Boolean,
+			default: true
+		},
+		//item text字体大小
+		textSize: {
+			type: Number,
+			default: 28
+		},
+		//text 是否不换行
+		nowrap: {
+			type: Boolean,
+			default: false
+		},
+		//item subText颜色
+		subTextColor: {
+			type: String,
+			default: '#999'
+		},
+		//item subText字体大小
+		subTextSize: {
+			type: Number,
+			default: 24
+		},
+		// item padding
+		padding: {
+			type: String,
+			default: '20rpx 30rpx'
+		},
+		//占位高度,第一条数据距离顶部距离
+		firstItemTop: {
+			type: String,
+			default: '20rpx'
+		},
+		//swiper 高度
+		height: {
+			type: String,
+			default: '300px'
+		},
+		//item  swiper 内容部分背景颜色
+		backgroundColor: {
+			type: String,
+			default: '#FFFFFF'
+		},
+		//子集数据是否请求返回(默认false,一次性返回所有数据)
+		request: {
+			type: Boolean,
+			default: false
+		},
+		//子级数据(当有改变时,默认当前选中项新增子级数据,request=true时生效)
+		receiveData: {
+			type: Array,
+			default: () => {
+				return [];
+			}
+		},
+		//改变值则重置数据
+		reset: {
+			type: [Number, String],
+			default: 0
+		}
+	},
+	watch: {
+		itemList(val) {
+			this.initData(val, -1);
+		},
+		receiveData(val) {
+			this.subLevelData(val, this.currentTab);
+		},
+		reset() {
+			this.initData(this.itemList, -1);
+		},
+		defaultItemList(val){
+			this.setDefaultData(val)
+		}
+	},
+	created() {
+		this.setDefaultData(this.defaultItemList)
+	},
+	data() {
+		return {
+			currentTab: 0,
+			//tab栏scrollview滚动的位置
+			scrollViewId: 'id__1',
+			selectedArr: []
+		};
+	},
+	methods: {
+		setDefaultData(val){
+			let defaultItemList = val || [];
+			if (defaultItemList.length > 0) {
+				defaultItemList.map(item => {
+					item.scrollViewId = `id_${item.index}`;
+				});
+				this.selectedArr = defaultItemList;
+				this.currentTab = defaultItemList.length - 1;
+				this.$nextTick(() => {
+					this.checkCor();
+				});
+			} else {
+				this.initData(this.itemList, -1);
+			}
+		},
+		initData(data, layer) {
+			if (!data || data.length === 0) return;
+			if (this.request) {
+				//第一级数据
+				this.subLevelData(data, layer);
+			} else {
+				let selectedValue = this.selectedValue || {};
+				if (selectedValue.type) {
+					this.setDefaultData(selectedValue);
+				} else {
+					this.subLevelData(this.getItemList(layer, -1), layer);
+				}
+			}
+		},
+		removeChildren(data) {
+			let list = data.map(item => {
+				delete item['children'];
+				return item;
+			});
+			return list;
+		},
+		getItemList(layer, index) {
+			let list = [];
+			let arr = JSON.parse(JSON.stringify(this.itemList));
+			if (layer == -1) {
+				list = this.removeChildren(arr);
+			} else {
+				let value = this.selectedArr[0].index;
+				value = value == -1 ? index : value;
+				list = arr[value].children || [];
+				if (layer > 0) {
+					for (let i = 1; i < layer + 1; i++) {
+						let val = layer === i ? index : this.selectedArr[i].index;
+						list = list[val].children || [];
+						if (list.length === 0) break;
+					}
+				}
+				list = this.removeChildren(list);
+			}
+			return list;
+		},
+		//滚动切换
+		switchTab: function(e) {
+			this.currentTab = e.detail.current;
+			this.checkCor();
+		},
+		//点击标题切换当
+		swichNav: function(e) {
+			let cur = e.currentTarget.dataset.current;
+			if (this.currentTab != cur) {
+				this.currentTab = cur;
+			}
+		},
+		checkCor: function() {
+			let item = this.selectedArr[this.currentTab];
+			item.scrollViewId = 'id__1';
+			this.$nextTick(() => {
+				setTimeout(() => {
+					let val = item.index < 2 ? 0 : Number(item.index - 2);
+					item.scrollViewId = `id_${val}`;
+				}, 2);
+			});
+			
+			if (this.currentTab > 1) {
+				this.scrollViewId = `id_${this.currentTab - 1}`;
+			} else {
+				this.scrollViewId = `id_0`;
+			}
+		},
+		change(index, subIndex, subItem) {
+			let item = this.selectedArr[index];
+			if (item.index == subIndex) return;
+			item.index = subIndex;
+			item.text = subItem.text;
+			item.value = subItem.value;
+			item.subText = subItem.subText || '';
+			item.src = subItem.src || '';
+
+			this.$emit('change', {
+				layer: index,
+				subIndex: subIndex, //layer=> Array index
+				...subItem
+			});
+
+			if (!this.request) {
+				let data = this.getItemList(index, subIndex);
+				this.subLevelData(data, index);
+			}
+		},
+		//新增子级数据时处理
+		subLevelData(data, layer) {
+			if (!data || data.length === 0) {
+				if (layer == -1) return;
+				//完成选择
+				let result = JSON.parse(JSON.stringify(this.selectedArr));
+				let lastItem = result[result.length - 1] || {};
+				let text = '';
+				result.map(item => {
+					text += item.text;
+					delete item['list'];
+					//delete item['index'];
+					delete item['scrollViewId'];
+					return item;
+				});
+				this.$emit('complete', {
+					result: result,
+					value: lastItem.value,
+					text: text,
+					subText: lastItem.subText,
+					src: lastItem.src
+				});
+			} else {
+				//重置数据( >layer层级)
+				let item = [
+					{
+						text: this.text,
+						subText: '',
+						value: '',
+						src: '',
+						index: -1,
+						scrollViewId: 'id__1',
+						list: data
+					}
+				];
+				if (layer == -1) {
+					this.selectedArr = item;
+				} else {
+					let retainArr = this.selectedArr.slice(0, layer + 1);
+					this.selectedArr = retainArr.concat(item);
+				}
+				this.$nextTick(() => {
+					this.currentTab = this.selectedArr.length - 1;
+				});
+			}
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-cascade-selection {
+	width: 100%;
+	box-sizing: border-box;
+}
+
+.tui-selection-header {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	position: relative;
+	box-sizing: border-box;
+}
+
+.tui-bottom-line {
+	position: relative;
+}
+
+.tui-bottom-line::after {
+	width: 100%;
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5) translateZ(0);
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 100%;
+	bottom: 0;
+	right: 0;
+	left: 0;
+}
+
+.tui-btm-none::after {
+	border-bottom: 0 !important;
+}
+
+.tui-header-item {
+	max-width: 240rpx;
+	padding: 15rpx 30rpx;
+	box-sizing: border-box;
+	flex-shrink: 0;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	position: relative;
+}
+
+.tui-font-bold {
+	font-weight: bold;
+}
+
+.tui-active-line {
+	width: 60rpx;
+	height: 6rpx;
+	border-radius: 4rpx;
+	position: absolute;
+	bottom: 0;
+	right: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
+
+.tui-selection-cell {
+	width: 100%;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+}
+
+.tui-icon-success {
+	margin-right: 12rpx;
+}
+
+.tui-cell-img {
+	margin-right: 12rpx;
+	flex-shrink: 0;
+}
+
+.tui-cell-title {
+	word-break: break-all;
+}
+
+.tui-flex-shrink {
+	flex-shrink: 0;
+}
+
+.tui-font-bold {
+	font-weight: bold;
+}
+
+.tui-cell-sub_title {
+	margin-left: 20rpx;
+	word-break: break-all;
+}
+.tui-first-item {
+	width: 100%;
+}
+</style>

+ 257 - 0
components/thorui/tui-circular-progress/tui-circular-progress.vue

@@ -0,0 +1,257 @@
+<template>
+	<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
+		<canvas class="tui-circular-default" :canvas-id="defaultCanvasId" :id="defaultCanvasId" :style="{ width: diam + 'px', height: (height || diam) + 'px' }"
+		 v-if="defaultShow"></canvas>
+		<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId" :style="{ width: diam + 'px', height: (height || diam) + 'px' }"></canvas>
+		<slot />
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiCircularProgress',
+		props: {
+			/*
+			  传值需使用rpx进行转换保证各终端兼容
+			  px = rpx / 750 * wx.getSystemInfoSync().windowWidth
+			  圆形进度条(画布)宽度,直径 [px]
+			*/
+			diam: {
+				type: Number,
+				default: 60
+			},
+			//圆形进度条(画布)高度,默认取diam值[当画半弧时传值,height有值时则取height]
+			height: {
+				type: Number,
+				default: 0
+			},
+			//进度条线条宽度[px]
+			lineWidth: {
+				type: Number,
+				default: 4
+			},
+			/*
+			 线条的端点样式
+			 butt:向线条的每个末端添加平直的边缘
+			 round	向线条的每个末端添加圆形线帽
+			 square	向线条的每个末端添加正方形线帽
+			*/
+			lineCap: {
+				type: String,
+				default: 'round'
+			},
+			//圆环进度字体大小 [px]
+			fontSize: {
+				type: Number,
+				default: 12
+			},
+			//圆环进度字体颜色
+			fontColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//是否显示进度文字
+			fontShow: {
+				type: Boolean,
+				default: true
+			},
+			/*
+			 自定义显示文字[默认为空,显示百分比,fontShow=true时生效]
+			 可以使用 slot自定义显示内容
+			*/
+			percentText: {
+				type: String,
+				default: ''
+			},
+			//是否显示默认(背景)进度条
+			defaultShow: {
+				type: Boolean,
+				default: true
+			},
+			//默认进度条颜色
+			defaultColor: {
+				type: String,
+				default: '#CCC'
+			},
+			//进度条颜色
+			progressColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//进度条渐变颜色[结合progressColor使用,默认为空]
+			gradualColor: {
+				type: String,
+				default: ''
+			},
+			//起始弧度,单位弧度
+			sAngle: {
+				type: Number,
+				default: -Math.PI / 2
+			},
+			//指定弧度的方向是逆时针还是顺时针。默认是false,即顺时针
+			counterclockwise: {
+				type: Boolean,
+				default: false
+			},
+			//进度百分比 [10% 传值 10]
+			percentage: {
+				type: Number,
+				default: 0
+			},
+			//进度百分比缩放倍数[使用半弧为100%时,则可传2]
+			multiple: {
+				type: Number,
+				default: 1
+			},
+			//动画执行时间[单位毫秒,低于50无动画]
+			duration: {
+				type: Number,
+				default: 800
+			},
+			//backwards: 动画从头播;forwards:动画从上次结束点接着播
+			activeMode: {
+				type: String,
+				default: 'backwards'
+			}
+		},
+		watch: {
+			percentage(val) {
+				this.initDraw()
+			}
+		},
+		data() {
+			return {
+				// #ifdef MP-WEIXIN
+				progressCanvasId:"progressCanvasId",
+				defaultCanvasId: "defaultCanvasId",
+				// #endif
+				// #ifndef MP-WEIXIN
+				progressCanvasId:this.getCanvasId(),
+				defaultCanvasId: this.getCanvasId(),
+				// #endif
+				progressContext: null,
+				linearGradient: null,
+				//起始百分比
+				startPercentage: 0
+				// dpi
+				//pixelRatio: uni.getSystemInfoSync().pixelRatio
+			};
+		},
+		mounted() {
+			this.initDraw(true)
+		},
+		methods: {
+			//初始化绘制
+			initDraw(init) {
+				let start = this.activeMode === 'backwards' ? 0 : this.startPercentage;
+				if (this.defaultShow && init) {
+					this.drawDefaultCircular();
+				}
+				this.drawProgressCircular(start);
+			},
+			//默认(背景)圆环
+			drawDefaultCircular() {
+				let ctx = uni.createCanvasContext(this.defaultCanvasId, this);
+				ctx.setLineWidth(this.lineWidth);
+				ctx.setStrokeStyle(this.defaultColor);
+				//终止弧度
+				let eAngle = Math.PI * (this.height ? 1 : 2) + this.sAngle;
+				this.drawArc(ctx, eAngle);
+			},
+			//进度圆环
+			drawProgressCircular(startPercentage) {
+				let ctx = this.progressContext;
+				let gradient = this.linearGradient;
+				if (!ctx) {
+					ctx = uni.createCanvasContext(this.progressCanvasId, this);
+					//创建一个线性的渐变颜色 CanvasGradient对象
+					gradient = ctx.createLinearGradient(0, 0, this.diam, 0);
+					gradient.addColorStop('0', this.progressColor);				
+					if (this.gradualColor) {
+						gradient.addColorStop('1', this.gradualColor);
+					}
+					// #ifdef APP-PLUS
+					const res = uni.getSystemInfoSync();
+					if (!this.gradualColor && res.platform.toLocaleLowerCase() == "android") {
+						gradient.addColorStop('1', this.progressColor);
+					}
+					// #endif
+					this.progressContext = ctx;
+					this.linearGradient = gradient;
+				}
+				ctx.setLineWidth(this.lineWidth);
+				ctx.setStrokeStyle(gradient);
+				let time = this.duration / this.percentage;
+				if (this.percentage > 0 || !this.fontShow) {
+					startPercentage = this.duration < 50 ? this.percentage - 1 : startPercentage;
+					startPercentage++;
+					if (startPercentage > this.percentage) {
+						this.$emit('end', {
+							canvasId: this.progressCanvasId
+						});
+						return;
+					}
+				}
+				if (this.fontShow) {
+					ctx.setFontSize(this.fontSize);
+					ctx.setFillStyle(this.fontColor);
+					ctx.setTextAlign('center');
+					ctx.setTextBaseline('middle');
+					let percentage = this.percentText;
+					if (!percentage) {
+						percentage = this.counterclockwise ? 100 - startPercentage * this.multiple : startPercentage * this.multiple;
+						percentage = `${percentage}%`;
+					}
+					let radius = this.diam / 2;
+					ctx.fillText(percentage, radius, radius);
+					if (this.percentage === 0 || (this.counterclockwise && startPercentage === 100)) {
+						ctx.draw();
+						return;
+					}
+				}
+				let eAngle = ((2 * Math.PI) / 100) * startPercentage + this.sAngle;
+				this.drawArc(ctx, eAngle);
+				setTimeout(() => {
+					this.startPercentage = startPercentage;
+					this.drawProgressCircular(startPercentage);
+					this.$emit('change', {
+						percentage: startPercentage
+					});
+				}, time);
+				// #ifdef H5
+				// requestAnimationFrame(()=>{})
+				// #endif
+			},
+			//创建弧线
+			drawArc(ctx, eAngle) {
+				ctx.setLineCap(this.lineCap);
+				ctx.beginPath();
+				let radius = this.diam / 2; //x=y
+				ctx.arc(radius, radius, radius - this.lineWidth, this.sAngle, eAngle, this.counterclockwise);
+				ctx.stroke();
+				ctx.draw();
+			},
+			//生成canvasId
+			getCanvasId() {
+				let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+					return (c === 'x' ? (Math.random() * 16) | 0 : 'r&0x3' | '0x8').toString(16);
+				});
+				return uuid;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-circular-container,
+	.tui-circular-default {
+		position: relative;
+	}
+
+	.tui-circular-progress {
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 10;
+	}
+</style>

+ 166 - 0
components/thorui/tui-collapse/tui-collapse.vue

@@ -0,0 +1,166 @@
+<template>
+	<view class="tui-collapse" :style="{backgroundColor:bgColor}">
+		<view class="tui-collapse-head" :style="{backgroundColor:hdBgColor}" @tap.stop="handleClick">
+			<view class="tui-header" :class="{'tui-opacity':disabled}">
+				<slot name="title"></slot>
+				<view class="tui-collapse-icon tui-icon-arrow" :class="{'tui-icon-active':isOpen}" :style="{color:arrowColor}" v-if="arrow"></view>
+			</view>
+		</view>
+		<view class="tui-collapse-body_box" :style="{backgroundColor:bdBgColor,height:isOpen?height:'0rpx'}">
+			<view class="tui-collapse-body" :class="{'tui-collapse-transform':height=='auto','tui-collapse-body_show':isOpen && height=='auto'}">
+				<slot name="content"></slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiCollapse",
+		props: {
+			//collapse背景颜色
+			bgColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//collapse-head 背景颜色
+			hdBgColor: {
+				type: String,
+				default: '#fff'
+			},
+			//collapse-body 背景颜色
+			bdBgColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//collapse-body实际高度 open时使用
+			height: {
+				type: String,
+				default: 'auto'
+			},
+			//索引
+			index: {
+				type: Number,
+				default: 0
+			},
+			//当前索引,index==current时展开
+			current: {
+				type: Number,
+				default: -1
+			},
+			// 是否禁用
+			disabled: {
+				type: [Boolean, String],
+				default: false
+			},
+			//是否带箭头
+			arrow: {
+				type: [Boolean, String],
+				default: true
+			},
+			//箭头颜色
+			arrowColor: {
+				type: String,
+				default: "#333"
+			}
+		},
+		watch: {
+			current() {
+				this.updateCurrentChange()
+			}
+		},
+		created() {
+			this.updateCurrentChange()
+		},
+		data() {
+			return {
+				isOpen: false
+			};
+		},
+		methods: {
+			updateCurrentChange() {
+				this.isOpen = this.index == this.current
+			},
+			handleClick() {
+				if (this.disabled) return;
+				this.$emit("click", {
+					index: Number(this.index)
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'tuiCollapse';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAQ4AA0AAAAABlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEHAAAABoAAAAciRx3B0dERUYAAAP8AAAAHgAAAB4AKQAKT1MvMgAAAaAAAABCAAAAVjxuR/JjbWFwAAAB9AAAAD4AAAFCAA/pq2dhc3AAAAP0AAAACAAAAAj//wADZ2x5ZgAAAkAAAABEAAAARCs1U/toZWFkAAABMAAAADAAAAA2FpaT+mhoZWEAAAFgAAAAHQAAACQHngOFaG10eAAAAeQAAAAPAAAAEAwAAEBsb2NhAAACNAAAAAoAAAAKACIAAG1heHAAAAGAAAAAHwAAACABDwAdbmFtZQAAAoQAAAFJAAACiCnmEVVwb3N0AAAD0AAAACMAAAA1DunpUnjaY2BkYGAAYja/oO54fpuvDNwsDCBwc4/6fzjtwNDNfICpBMjlYGACiQIAGVAKZnjaY2BkYGBu+N/AEMPCAALMBxgYGVABCwBVNgMsAAAAeNpjYGRgYGBhEGQA0QwMTEDMBYQMDP/BfAYACnYBLQB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PGJ4xMDf8b2CIYW5gaAAKM4LkANq9C9sAAHjaY2GAABYIdgAAAMAATQB42mNgYGBmgGAZBkYGELAB8hjBfBYGBSDNAoRA/jOG//8hpBQzVCUDIxsDjMnAyAQkmBhQASPDsAcAMCAGoQAAAAAAAAAAAAAAIgAAAAEAQACLA8ACdAAQAAAlASYiBhQXARYyNwE2NCYiBwIA/oYNIBkMAZcNIA0BlwwZIA3uAXoMGSAN/mkMDAGXDSAZDAB42n2QPU4DMRCFn/MHJBJCIKhdUQDa/JQpEyn0CKWjSDbekGjXXnmdSDkBLRUHoOUYHIAbINFyCl6WSZMia+3o85uZ57EBnOMbCv/fJe6EFY7xKFzBETLhKvUX4Rr5XbiOFj6FG9R/hJu4VQPhFi7UGx1U7YS7m9JtywpnGAhXcIon4Sr1lXCN/CpcxxU+hBvUv4SbGONXuIVrZakM4WEwQWCcQWOKDeMCMRwskjIG1qE59GYSzExPN3oRO5s4GyjvV2KXAx5oOeeAKe09t2a+Sif+YMuB1JhuHgVLtimNLiJ0KBtfLJzV3ahzsP2e7ba02L9rgTXH7FENbNT8Pdsz0khsDK+QkjXyMrekElOPaGus8btnKdbzXgiJTrzL9IjHmjR1OvduaeLA4ufyjBx9tLmSPfeoHD5jWQh5v91OxCCKXYY/k9hxGQAAAHjaY2BigAAuMMnIgA5YwKJMjExciUVF+eW6KfnleQAZ0wQyAAAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAMAAQAEAAAAAgAAAAB42mNgYGBkAIKrS9Q5QPTNPer/YTQAQ+0HIAAA) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-collapse-icon {
+		font-family: "tuiCollapse" !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+	}
+
+	.tui-icon-arrow:before {
+		content: "\e600";
+	}
+
+	.tui-icon-arrow {
+		font-size: 32rpx;
+		transform: rotate(0);
+		transform-origin: center center;
+		transition: all 0.3s;
+		position: absolute;
+		top: 50%;
+		margin-top: -8px;
+		right: 30rpx;
+	}
+
+	.tui-arrow-padding {
+		padding-right: 62rpx;
+		box-sizing: border-box;
+	}
+
+	.tui-icon-active {
+		transform: rotate(180deg);
+		transform-origin: center center;
+	}
+
+	.tui-header {
+		position: relative;
+		z-index: 2;
+	}
+   .tui-collapse-body_box{
+	   transition: all 0.25s;
+	   overflow: hidden;
+   }
+	.tui-collapse-body {
+		transition: all 0.25s;
+		overflow: hidden;
+		position: relative;
+		z-index: 1;
+	}
+
+	.tui-collapse-transform {
+		opacity: 0;
+		visibility: hidden;
+		-webkit-transform: translateY(-40%);
+		transform: translateY(-40%);
+	}
+
+	.tui-collapse-body_show {
+		opacity: 1;
+		visibility: visible;
+		-webkit-transform: translateY(0);
+		transform: translateY(0);
+	}
+
+	.tui-opacity {
+		opacity: 0.6;
+	}
+</style>

+ 335 - 0
components/thorui/tui-countdown/tui-countdown.vue

@@ -0,0 +1,335 @@
+<template>
+	<view class="tui-countdown-box">
+		<view class="tui-countdown-item" :style="{ background: backgroundColor, borderColor: borderColor, width: getWidth(d, width) + 'rpx', height: height + 'rpx' }" v-if="days">
+			<view class="tui-countdown-time" :class="[scale ? 'tui-countdown-scale' : '']" :style="{ fontSize: size + 'rpx', color: color, lineHeight: size + 'rpx' }">
+				{{ d }}
+			</view>
+		</view>
+		<view
+			class="tui-countdown-colon"
+			:class="{ 'tui-colon-pad': borderColor == 'transparent' }"
+			:style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+			v-if="days"
+		>
+			{{ isColon ? ':' : '天' }}
+		</view>
+		<view class="tui-countdown-item" :style="{ background: backgroundColor, borderColor: borderColor, width: getWidth(h, width) + 'rpx', height: height + 'rpx' }" v-if="hours">
+			<view class="tui-countdown-time" :class="[scale ? 'tui-countdown-scale' : '']" :style="{ fontSize: size + 'rpx', color: color, lineHeight: size + 'rpx' }">
+				{{ h }}
+			</view>
+		</view>
+		<view
+			class="tui-countdown-colon"
+			:class="{ 'tui-colon-pad': borderColor == 'transparent' }"
+			:style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+			v-if="hours"
+		>
+			{{ isColon ? ':' : '时' }}
+		</view>
+		<view
+			class="tui-countdown-item"
+			:style="{ background: backgroundColor, borderColor: borderColor, width: getWidth(i, width) + 'rpx', height: height + 'rpx' }"
+			v-if="minutes"
+		>
+			<view class="tui-countdown-time" :class="[scale ? 'tui-countdown-scale' : '']" :style="{ fontSize: size + 'rpx', color: color, lineHeight: size + 'rpx' }">
+				{{ i }}
+			</view>
+		</view>
+		<view
+			class="tui-countdown-colon"
+			:class="{ 'tui-colon-pad': borderColor == 'transparent' }"
+			:style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+			v-if="minutes"
+		>
+			{{ isColon ? ':' : '分' }}
+		</view>
+		<view
+			class="tui-countdown-item"
+			:style="{ background: backgroundColor, borderColor: borderColor, width: getWidth(s, width) + 'rpx', height: height + 'rpx' }"
+			v-if="seconds"
+		>
+			<view class="tui-countdown-time" :class="[scale ? 'tui-countdown-scale' : '']" :style="{ fontSize: size + 'rpx', color: color, lineHeight: size + 'rpx' }">
+				{{ s }}
+			</view>
+		</view>
+		<view
+			class="tui-countdown-colon"
+			:class="{ 'tui-colon-pad': borderColor == 'transparent' }"
+			:style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+			v-if="seconds && !isColon"
+		>
+			{{ unitEn ? 's' : '秒' }}
+		</view>
+
+		<view class="tui-countdown-colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }" v-if="seconds && isMs && isColon">.</view>
+		<view
+			class="tui-countdown__ms"
+			:style="{
+				background: backgroundColor,
+				borderColor: borderColor,
+				fontSize: msSize + 'rpx',
+				color: msColor,
+				height: height + 'rpx',
+				width: msWidth > 0 ? msWidth + 'rpx' : 'auto'
+			}"
+			v-if="seconds && isMs"
+		>
+			<view :class="{ 'tui-ms__list': ani }">
+				<view class="tui-ms__item" :style="{ height: height + 'rpx' }" v-for="(item, index) in ms" :key="index">
+					<view :class="[scale ? 'tui-countdown-scale' : '']">{{item}}</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiCountdown',
+	props: {
+		//数字框宽度
+		width: {
+			type: Number,
+			default: 32
+		},
+		//数字框高度
+		height: {
+			type: Number,
+			default: 32
+		},
+		//数字框border颜色
+		borderColor: {
+			type: String,
+			default: '#333'
+		},
+		//数字框背景颜色
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		},
+		//数字框字体大小
+		size: {
+			type: Number,
+			default: 24
+		},
+		//数字框字体颜色
+		color: {
+			type: String,
+			default: '#333'
+		},
+		//是否缩放 0.9
+		scale: {
+			type: Boolean,
+			default: false
+		},
+		//冒号大小
+		colonSize: {
+			type: Number,
+			default: 28
+		},
+		//冒号颜色
+		colonColor: {
+			type: String,
+			default: '#333'
+		},
+		//剩余时间 (单位:秒)
+		time: {
+			type: Number,
+			default: 0
+		},
+		//是否包含天
+		days: {
+			type: Boolean,
+			default: false
+		},
+		//是否包含小时
+		hours: {
+			type: Boolean,
+			default: true
+		},
+		//是否包含分钟
+		minutes: {
+			type: Boolean,
+			default: true
+		},
+		//是否包含秒
+		seconds: {
+			type: Boolean,
+			default: true
+		},
+		//单位用英文缩写表示 仅seconds秒数有效
+		unitEn: {
+			type: Boolean,
+			default: false
+		},
+		//是否展示为冒号,false为文字
+		isColon: {
+			type: Boolean,
+			default: true
+		},
+		//是否返回剩余时间
+		returnTime: {
+			type: Boolean,
+			default: false
+		},
+		//是否显示毫秒
+		isMs: {
+			type: Boolean,
+			default: false
+		},
+		msWidth: {
+			type: Number,
+			default: 32
+		},
+		msSize: {
+			type: Number,
+			default: 24
+		},
+		msColor: {
+			type: String,
+			default: '#333'
+		}
+	},
+	watch: {
+		time(val) {
+			this.clearTimer();
+			this.doLoop();
+		}
+	},
+	data() {
+		return {
+			countdown: null,
+			d: '0',
+			h: '00',
+			i: '00',
+			s: '00',
+			//此处若从9到1,结束需要特殊处理
+			ms: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+			ani: false
+		};
+	},
+	created() {
+		this.clearTimer();
+		this.doLoop();
+	},
+	beforeDestroy() {
+		this.clearTimer();
+	},
+	methods: {
+		getWidth: function(num, width) {
+			return num > 99 ? (width / 2) * num.toString().length : width;
+		},
+		clearTimer() {
+			clearInterval(this.countdown);
+			this.countdown = null;
+		},
+		endOfTime() {
+			this.ani = false;
+			this.clearTimer();
+			this.$emit('end', {});
+		},
+		doLoop: function() {
+			let seconds = this.time || 0;
+			this.ani = true;
+			this.countDown(seconds);
+			this.countdown = setInterval(() => {
+				seconds--;
+				if (seconds < 0) {
+					this.endOfTime();
+					return;
+				}
+				this.countDown(seconds);
+				if (this.returnTime) {
+					this.$emit('time', { seconds: seconds });
+				}
+			}, 1000);
+		},
+		countDown(seconds) {
+			let [day, hour, minute, second] = [0, 0, 0, 0];
+			if (seconds > 0) {
+				day = this.days ? Math.floor(seconds / (60 * 60 * 24)) : 0;
+				hour = this.hours ? Math.floor(seconds / (60 * 60)) - day * 24 : 0;
+				minute = this.minutes ? Math.floor(seconds / 60) - hour * 60 - day * 24 * 60 : 0;
+				second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
+			} else {
+				this.endOfTime();
+			}
+			hour = hour < 10 ? '0' + hour : hour;
+			minute = minute < 10 ? '0' + minute : minute;
+			second = second < 10 ? '0' + second : second;
+			this.d = day;
+			this.h = hour;
+			this.i = minute;
+			this.s = second;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-countdown-box {
+	display: flex;
+	align-items: center;
+}
+
+.tui-countdown-box {
+	display: flex;
+	align-items: center;
+}
+
+.tui-countdown-item {
+	border: 1rpx solid;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 6rpx;
+	white-space: nowrap;
+	transform: translateZ(0);
+}
+
+.tui-countdown-time {
+	margin: 0;
+	padding: 0;
+}
+
+.tui-countdown-colon {
+	display: flex;
+	justify-content: center;
+	padding: 0 5rpx;
+}
+
+.tui-colon-pad {
+	padding: 0 !important;
+}
+
+.tui-countdown-scale {
+	transform: scale(0.9);
+	transform-origin: center center;
+}
+.tui-countdown__ms {
+	border: 1rpx solid;
+	overflow: hidden;
+	border-radius: 6rpx;
+}
+
+/*ms使用css3代替js频繁更新操作,性能优化*/
+.tui-ms__list {
+	animation: loop 1s steps(10) infinite;
+}
+
+@keyframes loop {
+	from {
+		transform: translateY(0);
+	}
+
+	to {
+		transform: translateY(-100%);
+	}
+}
+
+.tui-ms__item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+</style>

+ 548 - 0
components/thorui/tui-datetime/tui-datetime.vue

@@ -0,0 +1,548 @@
+<template>
+	<view class="tui-datetime-picker">
+		<view class="tui-mask" :class="{ 'tui-mask-show': isShow }" @touchmove.stop.prevent="stop" catchtouchmove="stop" @tap="hide"></view>
+		<view class="tui-header" :class="{ 'tui-show': isShow }">
+			<view
+				class="tui-picker-header"
+				:class="{ 'tui-date-radius': radius }"
+				:style="{ backgroundColor: headerBackground }"
+				@touchmove.stop.prevent="stop"
+				catchtouchmove="stop"
+			>
+				<view class="tui-btn-picker" :style="{ color: cancelColor }" hover-class="tui-opacity" :hover-stay-time="150" @tap="hide">取消</view>
+				<view class="tui-btn-picker" :style="{ color: color }" hover-class="tui-opacity" :hover-stay-time="150" @tap="btnFix">确定</view>
+			</view>
+			<view class="tui-date-header" :style="{ backgroundColor: unitBackground }" v-if="unitTop">
+				<view class="tui-date-unit" v-if="type < 4 || type == 7">年</view>
+				<view class="tui-date-unit" v-if="type < 4 || type == 7">月</view>
+				<view class="tui-date-unit" v-if="type == 1 || type == 2 || type == 7">日</view>
+				<view class="tui-date-unit" v-if="type == 1 || type == 4 || type == 5 || type == 7">时</view>
+				<view class="tui-date-unit" v-if="type == 1 || type > 3">分</view>
+				<view class="tui-date-unit" v-if="type > 4">秒</view>
+			</view>
+			<view class="tui-picker-body" :style="{ backgroundColor: bodyBackground }">
+				<picker-view :value="value" @change="change" class="tui-picker-view">
+					<picker-view-column v-if="!reset && (type < 4 || type == 7)">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in years" :key="index">
+							{{ item }}
+							<text class="tui-unit-text" v-if="!unitTop">年</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type < 4 || type == 7)">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in months" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-unit-text" v-if="!unitTop">月</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type == 2 || type == 7)">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in days" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-unit-text" v-if="!unitTop">日</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type == 4 || type == 5 || type == 7)">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in hours" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-unit-text" v-if="!unitTop">时</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type > 3)">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in minutes" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-unit-text" v-if="!unitTop">分</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && type > 4">
+						<view class="tui-column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }" v-for="(item, index) in seconds" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-unit-text" v-if="!unitTop">秒</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiDatetime',
+	props: {
+		//1-日期+时间(年月日+时分) 2-日期(年月日) 3-日期(年月) 4-时间(时分) 5-时分秒 6-分秒 7-年月日 时分秒
+		type: {
+			type: Number,
+			default: 1
+		},
+		//年份区间
+		startYear: {
+			type: Number,
+			default: 1980
+		},
+		//年份区间
+		endYear: {
+			type: Number,
+			default: 2050
+		},
+		//"取消"字体颜色
+		cancelColor: {
+			type: String,
+			default: '#888'
+		},
+		//"确定"字体颜色
+		color: {
+			type: String,
+			default: '#5677fc'
+		},
+		//设置默认显示日期 2019-08-01 || 2019-08-01 17:01 || 2019/08/01
+		setDateTime: {
+			type: String,
+			default: ''
+		},
+		//单位置顶
+		unitTop: {
+			type: Boolean,
+			default: false
+		},
+		//圆角设置
+		radius: {
+			type: Boolean,
+			default: false
+		},
+		//头部背景色
+		headerBackground: {
+			type: String,
+			default: '#fff'
+		},
+		//根据实际调整,不建议使用深颜色
+		bodyBackground: {
+			type: String,
+			default: '#fff'
+		},
+		//单位置顶时,单位条背景色
+		unitBackground: {
+			type: String,
+			default: '#fff'
+		}
+	},
+	data() {
+		return {
+			isShow: false,
+			years: [],
+			months: [],
+			days: [],
+			hours: [],
+			minutes: [],
+			seconds: [],
+			year: 0,
+			month: 0,
+			day: 0,
+			hour: 0,
+			minute: 0,
+			second: 0,
+			startDate: '',
+			endDate: '',
+			value: [0, 0, 0, 0, 0, 0],
+			reset: false
+		};
+	},
+	mounted() {
+		this.initData();
+	},
+	computed: {
+		yearOrMonth() {
+			return `${this.year}-${this.month}`;
+		},
+		propsChange() {
+			return `${this.setDateTime}-${this.type}-${this.startYear}-${this.endYear}`;
+		}
+	},
+	watch: {
+		yearOrMonth() {
+			this.setDays();
+		},
+		propsChange() {
+			this.reset = true;
+			setTimeout(() => {
+				this.initData();
+			}, 10);
+		}
+	},
+	methods: {
+		stop() {},
+		formatNum: function(num) {
+			return num < 10 ? '0' + num : num + '';
+		},
+		generateArray: function(start, end) {
+			return Array.from(new Array(end + 1).keys()).slice(start);
+		},
+		getIndex: function(arr, val) {
+			let index = arr.indexOf(val);
+			return ~index ? index : 0;
+		},
+		//日期时间处理
+		initSelectValue() {
+			let fdate = this.setDateTime.replace(/\-/g, '/');
+			fdate = fdate && fdate.indexOf('/') == -1 ? `2020/01/01 ${fdate}` : fdate;
+			let time = null;
+			if (fdate) time = new Date(fdate);
+			else time = new Date();
+			this.year = time.getFullYear();
+			this.month = time.getMonth() + 1;
+			this.day = time.getDate();
+			this.hour = time.getHours();
+			this.minute = time.getMinutes();
+			this.second = time.getSeconds();
+		},
+		initData() {
+			this.initSelectValue();
+			this.reset = false;
+			switch (this.type) {
+				case 1:
+					this.value = [0, 0, 0, 0, 0];
+					this.setYears();
+					this.setMonths();
+					this.setDays();
+					this.setHours();
+					this.setMinutes();
+					break;
+				case 2:
+					this.value = [0, 0, 0];
+					this.setYears();
+					this.setMonths();
+					this.setDays();
+					break;
+				case 3:
+					this.value = [0, 0];
+					this.setYears();
+					this.setMonths();
+					break;
+				case 4:
+					this.value = [0, 0];
+					this.setHours();
+					this.setMinutes();
+					break;
+				case 5:
+					this.value = [0, 0, 0];
+					this.setHours();
+					this.setMinutes();
+					this.setSeconds();
+					break;
+				case 6:
+					this.value = [0, 0];
+					this.setMinutes();
+					this.setSeconds();
+					break;
+				case 7:
+					this.value = [0, 0, 0, 0, 0, 0];
+					this.setYears();
+					this.setMonths();
+					this.setDays();
+					this.setHours();
+					this.setMinutes();
+					this.setSeconds();
+					break;
+				default:
+					break;
+			}
+		},
+		setYears() {
+			this.years = this.generateArray(this.startYear, this.endYear);
+			setTimeout(() => {
+				this.$set(this.value, 0, this.getIndex(this.years, this.year));
+			}, 8);
+		},
+		setMonths() {
+			this.months = this.generateArray(1, 12);
+			setTimeout(() => {
+				this.$set(this.value, 1, this.getIndex(this.months, this.month));
+			}, 8);
+		},
+		setDays() {
+			if (this.type == 3 || this.type == 4) return;
+			let totalDays = new Date(this.year, this.month, 0).getDate();
+			this.days = this.generateArray(1, totalDays);
+			setTimeout(() => {
+				this.$set(this.value, 2, this.getIndex(this.days, this.day));
+			}, 8);
+		},
+		setHours() {
+			this.hours = this.generateArray(0, 23);
+			setTimeout(() => {
+				let index = this.type == 5 || this.type == 7 ? this.value.length - 3 : this.value.length - 2;
+				this.$set(this.value, index, this.getIndex(this.hours, this.hour));
+			}, 8);
+		},
+		setMinutes() {
+			this.minutes = this.generateArray(0, 59);
+			setTimeout(() => {
+				let index = this.type > 4 ? this.value.length - 2 : this.value.length - 1;
+				this.$set(this.value, index, this.getIndex(this.minutes, this.minute));
+			}, 8);
+		},
+		setSeconds() {
+			this.seconds = this.generateArray(0, 59);
+			setTimeout(() => {
+				this.$set(this.value, this.value.length - 1, this.getIndex(this.seconds, this.second));
+			}, 8);
+		},
+		show() {
+			setTimeout(() => {
+				this.isShow = true;
+			}, 50);
+		},
+		hide() {
+			this.isShow = false;
+			this.$emit('cancel', {});
+		},
+		change(e) {
+			this.value = e.detail.value;
+			switch (this.type) {
+				case 1:
+					this.year = this.years[this.value[0]];
+					this.month = this.months[this.value[1]];
+					this.day = this.days[this.value[2]];
+					this.hour = this.hours[this.value[3]];
+					this.minute = this.minutes[this.value[4]];
+					break;
+				case 2:
+					this.year = this.years[this.value[0]];
+					this.month = this.months[this.value[1]];
+					this.day = this.days[this.value[2]];
+					break;
+				case 3:
+					this.year = this.years[this.value[0]];
+					this.month = this.months[this.value[1]];
+					break;
+				case 4:
+					this.hour = this.hours[this.value[0]];
+					this.minute = this.minutes[this.value[1]];
+					break;
+				case 5:
+					this.hour = this.hours[this.value[0]];
+					this.minute = this.minutes[this.value[1]];
+					this.second = this.seconds[this.value[2]];
+					break;
+				case 6:
+					this.minute = this.minutes[this.value[0]];
+					this.second = this.seconds[this.value[1]];
+					break;
+				case 7:
+					this.year = this.years[this.value[0]];
+					this.month = this.months[this.value[1]];
+					this.day = this.days[this.value[2]];
+					this.hour = this.hours[this.value[3]];
+					this.minute = this.minutes[this.value[4]];
+					this.second = this.seconds[this.value[5]];
+					break;
+				default:
+					break;
+			}
+		},
+		btnFix() {
+			setTimeout(() => {
+				let result = {};
+				let year = this.year;
+				let month = this.formatNum(this.month || 0);
+				let day = this.formatNum(this.day || 0);
+				let hour = this.formatNum(this.hour || 0);
+				let minute = this.formatNum(this.minute || 0);
+				let second = this.formatNum(this.second || 0);
+				switch (this.type) {
+					case 1:
+						result = {
+							year: year,
+							month: month,
+							day: day,
+							hour: hour,
+							minute: minute,
+							result: `${year}-${month}-${day} ${hour}:${minute}`
+						};
+						break;
+					case 2:
+						result = {
+							year: year,
+							month: month,
+							day: day,
+							result: `${year}-${month}-${day}`
+						};
+						break;
+					case 3:
+						result = {
+							year: year,
+							month: month,
+							result: `${year}-${month}`
+						};
+						break;
+					case 4:
+						result = {
+							hour: hour,
+							minute: minute,
+							result: `${hour}:${minute}`
+						};
+						break;
+					case 5:
+						result = {
+							hour: hour,
+							minute: minute,
+							second: second,
+							result: `${hour}:${minute}:${second}`
+						};
+						break;
+					case 6:
+						result = {
+							minute: minute,
+							second: second,
+							result: `${minute}:${second}`
+						};
+						break;
+					case 7:
+						result = {
+							year: year,
+							month: month,
+							day: day,
+							hour: hour,
+							minute: minute,
+							second: second,
+							result: `${year}-${month}-${day} ${hour}:${minute}:${second}`
+						};
+						break;
+					default:
+						break;
+				}
+				this.$emit('confirm', result);
+				this.hide();
+			}, 80);
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-datetime-picker {
+	position: relative;
+	z-index: 996;
+}
+
+.tui-picker-view {
+	height: 100%;
+	box-sizing: border-box;
+}
+
+.tui-mask {
+	position: fixed;
+	z-index: 997;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	visibility: hidden;
+	opacity: 0;
+	transition: all 0.3s ease-in-out;
+}
+
+.tui-mask-show {
+	visibility: visible !important;
+	opacity: 1 !important;
+}
+
+.tui-header {
+	z-index: 998;
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	width: 100%;
+	transition: all 0.3s ease-in-out;
+	transform: translateY(100%);
+}
+
+.tui-date-header {
+	width: 100%;
+	height: 52rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	font-size: 26rpx;
+	line-height: 26rpx;
+	/* #ifdef MP */
+	box-shadow: 0 15rpx 10rpx -15rpx #efefef;
+	/* #endif */
+	/* #ifndef MP */
+	box-shadow: 0 15rpx 10rpx -15rpx #888;
+	/* #endif */
+	position: relative;
+	z-index: 2;
+}
+
+.tui-date-unit {
+	flex: 1;
+	text-align: center;
+}
+
+.tui-show {
+	transform: translateY(0);
+}
+
+.tui-picker-header {
+	width: 100%;
+	height: 90rpx;
+	padding: 0 40rpx;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	box-sizing: border-box;
+	font-size: 32rpx;
+	position: relative;
+}
+
+.tui-date-radius {
+	border-top-left-radius: 20rpx;
+	border-top-right-radius: 20rpx;
+	overflow: hidden;
+}
+
+.tui-picker-header::after {
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+	bottom: 0;
+	right: 0;
+	left: 0;
+}
+
+.tui-picker-body {
+	width: 100%;
+	height: 520rpx;
+	overflow: hidden;
+}
+
+.tui-column-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 36rpx;
+	color: #333;
+}
+
+.tui-font-size_32 {
+	font-size: 32rpx !important;
+}
+
+.tui-unit-text {
+	font-size: 24rpx !important;
+	padding-left: 8rpx;
+}
+
+.tui-btn-picker {
+	padding: 16rpx;
+	box-sizing: border-box;
+	text-align: center;
+	text-decoration: none;
+}
+
+.tui-opacity {
+	opacity: 0.5;
+}
+</style>

+ 103 - 0
components/thorui/tui-divider/tui-divider.vue

@@ -0,0 +1,103 @@
+<template>
+	<view class="tui-divider" :style="{ height: height + 'rpx' }">
+		<view class="tui-divider-line" :style="{ width: width, background: getBgColor(gradual, gradualColor, dividerColor) }"></view>
+		<view
+			class="tui-divider-text"
+			:style="{ color: color, fontSize: size + 'rpx', lineHeight: size + 'rpx', backgroundColor: backgroundColor, fontWeight: bold ? 'bold' : 'normal' }"
+		>
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiDivider',
+	props: {
+		//divider占据高度
+		height: {
+			type: Number,
+			default: 100
+		},
+		//divider宽度,可填写具体长度,如400rpx
+		width: {
+			type: String,
+			default: '100%'
+		},
+		//divider颜色,如果为渐变线条,此属性失效
+		dividerColor: {
+			type: String,
+			default: '#e5e5e5'
+		},
+		//文字颜色
+		color: {
+			type: String,
+			default: '#999'
+		},
+		//文字大小 rpx
+		size: {
+			type: Number,
+			default: 24
+		},
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		//背景颜色,和当前页面背景色保持一致
+		backgroundColor: {
+			type: String,
+			default: '#fafafa'
+		},
+		//是否为渐变线条,为true,divideColor失效
+		gradual: {
+			type: Boolean,
+			default: false
+		},
+		//渐变色值,to right ,提供两个色值即可,由浅至深
+		gradualColor: {
+			type: Array,
+			default: function() {
+				return ['#eee', '#ccc'];
+			}
+		}
+	},
+	methods: {
+		getBgColor: function(gradual, gradualColor, dividerColor) {
+			let bgColor = dividerColor;
+			if (gradual) {
+				bgColor = 'linear-gradient(to right,' + gradualColor[0] + ',' + gradualColor[1] + ',' + gradualColor[1] + ',' + gradualColor[0] + ')';
+			}
+			return bgColor;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-divider {
+	width: 100%;
+	position: relative;
+	text-align: center;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	box-sizing: border-box;
+	overflow: hidden;
+}
+
+.tui-divider-line {
+	position: absolute;
+	height: 1rpx;
+	top: 50%;
+	left: 50%;
+	-webkit-transform: scaleY(0.5) translateX(-50%) translateZ(0);
+	transform: scaleY(0.5) translateX(-50%) translateZ(0);
+}
+
+.tui-divider-text {
+	position: relative;
+	text-align: center;
+	padding: 0 18rpx;
+	z-index: 1;
+}
+</style>

+ 139 - 0
components/thorui/tui-drawer/tui-drawer.vue

@@ -0,0 +1,139 @@
+<template>
+	<!-- @touchmove.stop.prevent -->
+	<view>
+		<view v-if="mask" class="tui-drawer-mask" :class="{ 'tui-drawer-mask_show': visible }" :style="{ zIndex: maskZIndex }" @tap="handleMaskClick"></view>
+		<view
+			class="tui-drawer-container"
+			:class="[`tui-drawer-container_${mode}`, visible ? `tui-drawer-${mode}__show` : '']"
+			:style="{ zIndex: zIndex, backgroundColor: backgroundColor }"
+		>
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 超过一屏时插槽使用scroll-view
+ **/
+export default {
+	name: 'tuiDrawer',
+	props: {
+		visible: {
+			type: Boolean,
+			default: false
+		},
+		mask: {
+			type: Boolean,
+			default: true
+		},
+		maskClosable: {
+			type: Boolean,
+			default: true
+		},
+		// left right bottom top
+		mode: {
+			type: String,
+			default: 'right'
+		},
+		//drawer z-index
+		zIndex: {
+			type: [Number, String],
+			default: 9999
+		},
+		//mask z-index
+		maskZIndex: {
+			type: [Number, String],
+			default: 9998
+		},
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		}
+	},
+	methods: {
+		handleMaskClick() {
+			if (!this.maskClosable) {
+				return;
+			}
+			this.$emit('close', {});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-drawer-mask {
+	opacity: 0;
+	visibility: hidden;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	transition: all 0.3s ease-in-out;
+}
+.tui-drawer-mask_show {
+	display: block;
+	visibility: visible;
+	opacity: 1;
+}
+
+.tui-drawer-container {
+	position: fixed;
+	left: 50%;
+	height: 100.2%;
+	top: 0;
+	transform: translate3d(-50%, -50%, 0);
+	transform-origin: center;
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	overflow-y: scroll;
+	-webkit-overflow-scrolling: touch;
+	-ms-touch-action: pan-y cross-slide-y;
+	-ms-scroll-chaining: none;
+	-ms-scroll-limit: 0 50 0 50;
+}
+.tui-drawer-container_left {
+	left: 0;
+	top: 50%;
+	transform: translate3d(-100%, -50%, 0);
+}
+
+.tui-drawer-container_right {
+	right: 0;
+	top: 50%;
+	left: auto;
+	transform: translate3d(100%, -50%, 0);
+}
+
+.tui-drawer-container_bottom,
+.tui-drawer-container_top {
+	width: 100%;
+	height: auto !important;
+	min-height: 20rpx;
+	left: 0;
+	right: 0;
+	transform-origin: center;
+	transition: all 0.3s ease-in-out;
+}
+.tui-drawer-container_bottom {
+	bottom: 0;
+	top: auto;
+	transform: translate3d(0, 100%, 0);
+}
+.tui-drawer-container_top {
+	transform: translate3d(0, -100%, 0);
+}
+.tui-drawer-left__show,
+.tui-drawer-right__show {
+	opacity: 1;
+	transform: translate3d(0, -50%, 0);
+}
+.tui-drawer-top__show,
+.tui-drawer-bottom__show {
+	opacity: 1;
+	transform: translate3d(0, 0, 0);
+}
+</style>

+ 69 - 0
components/thorui/tui-dropdown-list/tui-dropdown-list.vue

@@ -0,0 +1,69 @@
+<template>
+	<view class="tui-selected-class tui-dropdown-list" :style="{ height: selectHeight ? selectHeight + 'rpx' : 'auto' }">
+		<slot name="selectionbox"></slot>
+		<view
+			class="tui-dropdown-view"
+			:class="[show ? 'tui-dropdownlist-show' : '']"
+			:style="{ backgroundColor: backgroundColor, height: show ? height + 'rpx' : 0, top: top + 'rpx' }"
+		>
+			<slot name="dropdownbox"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiDropdownList',
+	props: {
+		//控制显示
+		show: {
+			type: Boolean,
+			default: false
+		},
+		//背景颜色
+		backgroundColor: {
+			type: String,
+			default: 'transparent'
+		},
+		//top  rpx
+		top: {
+			type: Number,
+			default: 0
+		},
+		//下拉框高度 rpx
+		height: {
+			type: Number,
+			default: 0
+		},
+		//选择框高度 单位rpx
+		selectHeight: {
+			type: Number,
+			default: 0
+		}
+	},
+	methods: {}
+};
+</script>
+
+<style scoped>
+.tui-dropdown-list {
+	position: relative;
+}
+
+.tui-dropdown-view {
+	width: 100%;
+	overflow: hidden;
+	position: absolute;
+	z-index: -99;
+	left: 0;
+	opacity: 0;
+	/* visibility: hidden; */
+	transition: all 0.2s ease-in-out;
+}
+
+.tui-dropdownlist-show {
+	opacity: 1;
+	z-index: 996;
+	/* visibility: visible; */
+}
+</style>

+ 261 - 0
components/thorui/tui-fab/tui-fab.vue

@@ -0,0 +1,261 @@
+<template>
+	<view @touchmove.stop.prevent>
+		<view class="tui-fab-box" :class="{'tui-fab-right':!left || (left && right)}" :style="{left:getLeft(),right:getRight(),bottom:bottom+'rpx'}">
+			<view class="tui-fab-btn" :class="{'tui-visible':isOpen,'tui-fab-hidden':hidden}">
+				<view class="tui-fab-item-box" :class="{'tui-fab-item-left':left && !right && item.imgUrl}" v-for="(item,index) in btnList"
+				 :key="index" @tap.stop="handleClick(index)">
+					<view :class="[left && !right?'tui-text-left':'tui-text-right']" v-if="item.imgUrl" :style="{fontSize:item.fontSize+'rpx',color:item.color}">{{item.text || ""}}</view>
+					<view class="tui-fab-item" :style="{width:width+'rpx',height:height+'rpx',background:item.bgColor || bgColor,borderRadius:radius}">
+						<view class="tui-fab-title" v-if="!item.imgUrl" :style="{fontSize:item.fontSize+'rpx',color:item.color}">{{item.text || ""}}</view>
+						<image :src="item.imgUrl" class="tui-fab-img" v-else :style="{width:item.imgWidth+'rpx',height:item.imgHeight+'rpx'}"></image>
+					</view>
+				</view>
+			</view>
+			<view class="tui-fab-item" :class="{'tui-active':isOpen}" :style="{width:width+'rpx',height:height+'rpx',borderRadius:radius,background:bgColor,color:color}"
+			 @tap.stop="handleClick(-1)">
+				<text class="tui-fab-icon tui-icon-plus" v-if="!custom"></text>
+				<slot></slot>
+			</view>
+		</view>
+		<view class="tui-fab-mask" :class="{'tui-visible':isOpen}" @tap="handleClickCancel"></view>
+	</view>
+</template>
+
+<script>
+	//拓展出来的按钮不应多于6个,否则违反了作为悬浮按钮的快速、高效的原则
+	export default {
+		name: "tuiFab",
+		props: {
+			//rpx 为0时值为auto
+			left: {
+				type: Number,
+				default: 0
+			},
+			//rpx 当为0时且left不为0,值为auto
+			right: {
+				type: Number,
+				default: 80
+			},
+			//rpx bottom值
+			bottom: {
+				type: Number,
+				default: 100
+			},
+			//默认按钮 宽度 rpx
+			width: {
+				type: Number,
+				default: 108
+			},
+			//默认按钮 高度 rpx
+			height: {
+				type: Number,
+				default: 108
+			},
+			//圆角值
+			radius: {
+				type: String,
+				default: "50%"
+			},
+			//默认按钮自定义内容[替换加号]
+			custom:{
+				type:Boolean,
+				default:false
+			},
+			//默认按钮背景颜色
+			bgColor: {
+				type: String,
+				default: "#5677fc"
+			},
+			//字体颜色
+			color: {
+				type: String,
+				default: "#fff"
+			},
+			//拓展按钮
+			// bgColor: "#5677fc",
+			// //图标/图片地址
+			// imgUrl: "/static/images/fab/fab_reward.png",
+			// //图片高度 rpx
+			// imgHeight: 60,
+			// //图片宽度 rpx
+			// imgWidth: 60,
+			// //名称
+			// text: "名称",
+			// //字体大小
+			// fontSize: 30,
+			// //字体颜色
+			// color: "#fff"
+			btnList: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//点击遮罩 是否可关闭
+			maskClosable: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				isOpen: false,
+				hidden: true,
+				timer: null
+			};
+		},
+		methods: {
+			getLeft() {
+				let val = "auto"
+				if (this.left && !this.right) {
+					val = this.left + 'rpx'
+				}
+				return val
+			},
+			getRight() {
+				let val = this.right + 'rpx'
+				if (this.left && !this.right) {
+					val = "auto"
+				}
+				return val
+			},
+			handleClick: function(index) {
+				this.hidden = false
+				clearTimeout(this.timer)
+				if (index == -1 && this.btnList.length) {
+					this.isOpen = !this.isOpen
+				} else {
+					this.$emit("click", {
+						index: index
+					})
+					this.isOpen = false
+				}
+				if (!this.isOpen) {
+					this.timer = setTimeout(() => {
+						this.hidden = true
+					}, 200)
+				}
+			},
+			handleClickCancel: function() {
+				if (!this.maskClosable) return;
+				this.isOpen = false
+			}
+		},
+		beforeDestroy() {
+			clearTimeout(this.timer)
+			this.timer = null
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'tuifab';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAREAA0AAAAABnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEKAAAABoAAAAciPExJUdERUYAAAQIAAAAHgAAAB4AKQAKT1MvMgAAAaAAAABCAAAAVjyBSAVjbWFwAAAB9AAAAD4AAAFCAA/pvmdhc3AAAAQAAAAACAAAAAj//wADZ2x5ZgAAAkAAAABRAAAAYFkYQQNoZWFkAAABMAAAADAAAAA2Fm5OF2hoZWEAAAFgAAAAHQAAACQH3QOFaG10eAAAAeQAAAAPAAAAEAwAAANsb2NhAAACNAAAAAoAAAAKADAAAG1heHAAAAGAAAAAHwAAACABDwAobmFtZQAAApQAAAFJAAACiCnmEVVwb3N0AAAD4AAAAB8AAAAx2XRuznjaY2BkYGAAYtGolt54fpuvDNwsDCBwc1krH5xm/t/I/J+5FsjlYGACiQIAGAEKZHjaY2BkYGBu+N/AEMPCAALM/xkYGVABCwBZ4wNrAAAAeNpjYGRgYGBhkGEA0QwMTEDMBYQMDP/BfAYAC4kBOAB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PhJ8JMzf8b2CIYW5gaAAKM4LkAN21DAEAAHjaY2GAABYIZgYAAIMAEAB42mNgYGBmgGAZBkYGELAB8hjBfBYGBSDNAoRA/jPh//8hpOQHqEoGRjYGGJOBkQlIMDGgAkaGYQ8AUSIHswAAAAAAAAAAAAAAMAAAeNpjYGRg/t/I/J+5lkGagYFRUVCPUYmNXVCRj1FETFxRUI7RyMxcUNGO0USN+fS/HEY5XTnGfznicnLijFPAHMYpYnJyjFvBlBgWBQBNJxKpAAAAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMCiTIxMbFmZiRmJ+QALXAKKAAAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAMAAQAEAAAAAgAAAAB42mNgYGBkAIKrS9Q5QPTNZa18MBoAPbcFzgAA) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-fab-icon {
+		font-family: "tuifab" !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+		padding: 10rpx;
+	}
+
+	.tui-icon-plus:before {
+		content: "\e613";
+	}
+
+	.tui-fab-box {
+		display: flex;
+		justify-content: center;
+		flex-direction: column;
+		position: fixed;
+		z-index: 99997;
+	}
+
+	.tui-fab-right {
+		align-items: flex-end;
+	}
+
+	.tui-fab-btn {
+		transform: scale(0);
+		transition: all 0.2s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-fab-hidden {
+		height: 0;
+		width: 0;
+	}
+
+
+	.tui-fab-item-box {
+		display: flex;
+		align-items: center;
+		justify-content: flex-end;
+		padding-bottom: 40rpx;
+	}
+
+	.tui-fab-item-left {
+		flex-flow: row-reverse;
+	}
+
+	.tui-fab-title {
+		width: 90%;
+		text-align: center;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.tui-text-left {
+		padding-left: 28rpx;
+	}
+
+	.tui-text-right {
+		padding-right: 28rpx;
+	}
+
+	.tui-fab-img {
+		display: block;
+	}
+
+	.tui-fab-item {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1);
+		transition: all 0.2s linear;
+	}
+
+	.tui-radius {
+		border-radius: 50%;
+	}
+
+	.tui-active {
+		transform: rotate(135deg);
+	}
+
+	.tui-fab-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.75);
+		z-index: 99996;
+		transition: all 0.2s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-visible {
+		visibility: visible;
+		opacity: 1;
+		transform: scale(1);
+	}
+</style>

+ 118 - 0
components/thorui/tui-footer/tui-footer.vue

@@ -0,0 +1,118 @@
+<template>
+	<view class="tui-footer-class tui-footer" :class="[fixed?'tui-fixed':'']" :style='{backgroundColor:backgroundColor}'>
+		<view class="tui-footer-link" v-if="navigate.length>0">
+			<block v-for="(item,index) in navigate" :key="index">
+				<navigator class="tui-link" hover-class="tui-link-hover" :hover-stop-propagation="true" :style="{color:(item.color || '#596d96'),fontSize:(item.size || 28)+'rpx'}"
+				 :open-type="item.type" :url="item.url" :target="item.target" :delta="item.delta" :app-id="item.appid"
+				 :path="item.path" :extra-data="item.extradata" :bindsuccess="item.bindsuccess" :bindfail="item.bindfail">{{item.text}}</navigator>
+			</block>
+		</view>
+		<view class="tui-footer-copyright" :style="{color:color,fontSize:size+'rpx'}">
+			{{copyright}}
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiFooter",
+		props: {
+			//type target url delta appid path extradata bindsuccess bindfail text color size
+			//链接设置  数据格式对应上面注释的属性值
+			navigate: {
+				type: Array,
+				default:function(){
+					return  []
+				}
+			},
+			//底部文本
+			copyright: {
+				type: String,
+				default: "All Rights Reserved."
+			},
+			//copyright 字体颜色
+			color: {
+				type: String,
+				default: "#A7A7A7"
+			},
+			//copyright 字体大小
+			size: {
+				type: Number,
+				default: 24
+			},
+			//footer背景颜色
+			backgroundColor: {
+				type: String,
+				default: "transparent"
+			},
+			//是否固定在底部
+			fixed: {
+				type: Boolean,
+				default: true
+			}
+		},
+		methods: {
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-footer {
+		width: 100%;
+		overflow: hidden;
+		padding: 30rpx 24rpx;
+		box-sizing: border-box;
+	}
+
+	.tui-fixed {
+		position: fixed;
+		z-index: 9999;
+		bottom: 0;
+		left: 0;
+	}
+
+	.tui-footer-link {
+		color: #596d96;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 28rpx;
+	}
+
+	.tui-link {
+		position: relative;
+		padding: 0 18rpx;
+		line-height: 1;
+	}
+
+	.tui-link::before {
+		content: " ";
+		position: absolute;
+		right: 0;
+		top: 0;
+		width: 1px;
+		bottom: 0;
+		border-right: 1px solid #d3d3d3;
+		-webkit-transform-origin: 100% 0;
+		transform-origin: 100% 0;
+		-webkit-transform: scaleX(0.5);
+		transform: scaleX(0.5);
+	}
+
+	.tui-link:last-child::before {
+		border-right: 0 !important
+	}
+
+	.tui-link-hover {
+		opacity: 0.5
+	}
+
+	.tui-footer-copyright {
+		font-size: 24rpx;
+		color: #A7A7A7;
+		line-height: 1;
+		text-align: center;
+		padding-top: 16rpx;
+		padding-bottom:env(safe-area-inset-bottom);
+	}
+</style>

+ 147 - 0
components/thorui/tui-grid-item/tui-grid-item.vue

@@ -0,0 +1,147 @@
+<template>
+	<view class="tui-grid" :class="[bottomLine?'':'tui-grid-bottom',border?'':'tui-grid__unlined','tui-grid-'+(cell<2?3:cell)]" :hover-class="hover?'tui-item-hover':''"
+	 :hover-stay-time="150" :style="{backgroundColor:backgroundColor}" @tap="handleClick">
+		<view class='tui-grid-bg'>
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiGridItem",
+		props: {
+			cell: {
+				type: [Number,String],
+				default: 3
+			},
+			backgroundColor: {
+				type: String,
+				default: "#fff"
+			},
+			//是否有点击效果
+			hover: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要底部线条
+			bottomLine: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要纵向边框线条
+			border:{
+				type: Boolean,
+				default: true
+			},
+			index: {
+				type: Number,
+				default: 0
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {
+					index: this.index
+				});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-grid {
+		position: relative;
+		padding: 40rpx 20rpx;
+		box-sizing: border-box;
+		background: #fff;
+		float: left;
+	}
+
+	.tui-grid-2 {
+		width: 50%;
+	}
+
+	.tui-grid-3 {
+		width: 33.333333333%;
+	}
+
+	.tui-grid-4 {
+		width: 25%;
+		padding: 30rpx 20rpx !important;
+	}
+
+	.tui-grid-5 {
+		width: 20%;
+		padding: 20rpx !important;
+	}
+
+	.tui-grid-2:nth-of-type(2n)::before {
+		width: 0;
+		border-right: 0;
+	}
+
+	.tui-grid-3:nth-of-type(3n)::before {
+		width: 0;
+		border-right: 0;
+	}
+
+	.tui-grid-4:nth-of-type(4n)::before {
+		width: 0;
+		border-right: 0;
+	}
+
+	.tui-grid-5:nth-of-type(5n)::before {
+		width: 0;
+		border-right: 0;
+	}
+
+	.tui-grid::before {
+		content: " ";
+		position: absolute;
+		right: 0;
+		top: 0;
+		width: 1px;
+		bottom: 0;
+		border-right: 1px solid #eaeef1;
+		-webkit-transform-origin: 100% 0;
+		transform-origin: 100% 0;
+		-webkit-transform: scaleX(0.5);
+		transform: scaleX(0.5);
+	}
+	
+	.tui-grid__unlined::before{
+		width: 0 !important;
+		border-right: 0 !important;
+	}
+
+	.tui-grid::after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		height: 1px;
+		border-bottom: 1px solid #eaeef1;
+		-webkit-transform-origin: 0 100%;
+		transform-origin: 0 100%;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+	}
+
+	.tui-grid-bottom::after {
+		height: 0 !important;
+		border-bottom: 0 !important
+	}
+
+	.tui-grid-bg {
+		position: relative;
+		padding: 0;
+		width: 100%;
+		box-sizing: border-box;
+	}
+
+	.tui-item-hover {
+		background-color: #f7f7f9 !important;
+	}
+</style>

+ 44 - 0
components/thorui/tui-grid/tui-grid.vue

@@ -0,0 +1,44 @@
+<template>
+	<view class="tui-grids" :class="{'tui-border-top':unlined}">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:"tuiGrid",
+		props: {
+			//是否去掉上线条
+			unlined: {
+				type: Boolean,
+				default: false
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-grids {
+		width: 100%;
+		position: relative;
+		overflow: hidden;
+	}
+
+	.tui-grids::after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 1px;
+		border-top: 1px solid #eaeef1;
+		-webkit-transform-origin: 0 0;
+		transform-origin: 0 0;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+	}
+
+	.tui-border-top::after {
+		border-top: 0 !important;
+	}
+</style>

File diff suppressed because it is too large
+ 55 - 0
components/thorui/tui-icon/tui-icon.vue


+ 1030 - 0
components/thorui/tui-image-cropper/tui-image-cropper.vue

@@ -0,0 +1,1030 @@
+<template>
+	<view class="tui-container" @touchmove.stop.prevent="stop">
+		<view class="tui-image-cropper" @touchend="cutTouchEnd" @touchstart="cutTouchStart" @touchmove="cutTouchMove">
+			<view class="tui-content">
+				<view class="tui-content-top tui-bg-transparent" :style="{ height: cutY + 'px', transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				<view class="tui-content-middle" :style="{ height: canvasHeight + 'px' }">
+					<view class="tui-bg-transparent" :style="{ width: cutX + 'px', transitionProperty: cutAnimation ? '' : 'background' }"></view>
+					<view class="tui-cropper-box" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', borderColor: borderColor, transitionProperty: cutAnimation ? '' : 'background' }">
+						<view v-for="(item, index) in 4" :key="index" class="tui-edge" :class="[`tui-${index < 2 ? 'top' : 'bottom'}-${index === 0 || index === 2 ? 'left' : 'right'}`]"
+						 :style="{
+								width: edgeWidth,
+								height: edgeWidth,
+								borderColor: edgeColor,
+								borderWidth: edgeBorderWidth,
+								left: index === 0 || index === 2 ? `-${edgeOffsets}` : 'auto',
+								right: index === 1 || index === 3 ? `-${edgeOffsets}` : 'auto',
+								top: index < 2 ? `-${edgeOffsets}` : 'auto',
+								bottom: index > 1 ? `-${edgeOffsets}` : 'auto'
+							}"></view>
+					</view>
+					<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				</view>
+				<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+			</view>
+			<image @load="imageLoad" @error="imageLoad" @touchstart="start" @touchmove="move" @touchend="end" :style="{
+					width: imgWidth ? imgWidth + 'px' : 'auto',
+					height: imgHeight ? imgHeight + 'px' : 'auto',
+					transform: imgTransform,
+					transitionDuration: (cutAnimation ? 0.35 : 0) + 's'
+				}"
+			 class="tui-cropper-image" :src="imageUrl" v-if="imageUrl" mode="widthFix"></image>
+		</view>
+		<canvas canvas-id="tui-image-cropper" id="tui-image-cropper" :disable-scroll="true" :style="{ width: CROPPER_WIDTH * scaleRatio + 'px', height: CROPPER_HEIGHT * scaleRatio + 'px' }"
+		 class="tui-cropper-canvas"></canvas>
+		<view class="tui-cropper-tabbar" v-if="!custom">
+			<view class="tui-op-btn" @tap.stop="back">取消</view>
+			<image :src="rotateImg" class="tui-rotate-img" @tap="setAngle"></image>
+			<view class="tui-op-btn" @tap.stop="getImage">完成</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 注意:组件中使用的图片地址,将文件复制到自己项目中
+	 * 如果图片位置与组件同级,编译成小程序时图片会丢失
+	 * 拷贝static下整个components文件夹
+	 *也可直接转成base64(不建议)
+	 * */
+	export default {
+		name: 'tuiImageCropper',
+		props: {
+			//图片路径
+			imageUrl: {
+				type: String,
+				default: ''
+			},
+			/*
+				 默认正方形,可修改大小控制比例
+				 裁剪框高度 px
+				*/
+			height: {
+				type: Number,
+				default: 280
+			},
+			//裁剪框宽度 px
+			width: {
+				type: Number,
+				default: 280
+			},
+			//裁剪框最小宽度 px
+			minWidth: {
+				type: Number,
+				default: 100
+			},
+			//裁剪框最小高度 px
+			minHeight: {
+				type: Number,
+				default: 100
+			},
+			//裁剪框最大宽度 px
+			maxWidth: {
+				type: Number,
+				default: 360
+			},
+			//裁剪框最大高度 px
+			maxHeight: {
+				type: Number,
+				default: 360
+			},
+			//裁剪框border颜色
+			borderColor: {
+				type: String,
+				default: 'rgba(255,255,255,0.1)'
+			},
+			//裁剪框边缘线颜色
+			edgeColor: {
+				type: String,
+				default: '#FFFFFF'
+			},
+			//裁剪框边缘线宽度 w=h
+			edgeWidth: {
+				type: String,
+				default: '34rpx'
+			},
+			//裁剪框边缘线border宽度
+			edgeBorderWidth: {
+				type: String,
+				default: '6rpx'
+			},
+			//偏移距离,根据edgeBorderWidth进行调整
+			edgeOffsets: {
+				type: String,
+				default: '6rpx'
+			},
+			/**
+			 * 如果宽度和高度都为true则裁剪框禁止拖动
+			 * 裁剪框宽度锁定
+			 */
+			lockWidth: {
+				type: Boolean,
+				default: false
+			},
+			//裁剪框高度锁定
+			lockHeight: {
+				type: Boolean,
+				default: false
+			},
+			//锁定裁剪框比例(放大或缩小)
+			lockRatio: {
+				type: Boolean,
+				default: false
+			},
+			//生成的图片尺寸相对剪裁框的比例
+			scaleRatio: {
+				type: Number,
+				default: 2
+			},
+			//图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
+			quality: {
+				type: Number,
+				default: 0.8
+			},
+			//图片旋转角度
+			rotateAngle: {
+				type: Number,
+				default: 0
+			},
+			//图片最小缩放比
+			minScale: {
+				type: Number,
+				default: 0.5
+			},
+			//图片最大缩放比
+			maxScale: {
+				type: Number,
+				default: 2
+			},
+			//是否禁用触摸旋转(为false则可以触摸转动图片,limitMove为false生效)
+			disableRotate: {
+				type: Boolean,
+				default: true
+			},
+			//是否限制移动范围(剪裁框只能在图片内,为true不可触摸转动图片)
+			limitMove: {
+				type: Boolean,
+				default: true
+			},
+			//自定义操作栏(为true时隐藏底部操作栏)
+			custom: {
+				type: Boolean,
+				default: false
+			},
+			//值发生改变开始裁剪(custom为true时生效)
+			startCutting: {
+				type: [Number, Boolean],
+				default: 0
+			},
+			/**
+			 * 是否返回base64(H5端默认base64)
+			 * 支持平台:App,微信小程序,支付宝小程序,H5(默认url就是base64)
+			 **/
+			isBase64: {
+				type: Boolean,
+				default: false
+			},
+			//裁剪时是否显示loadding
+			loadding: {
+				type: Boolean,
+				default: true
+			},
+			//旋转icon
+			rotateImg: {
+				type: String,
+				default: '/static/components/cropper/img_rotate.png'
+			}
+		},
+		data() {
+			return {
+				MOVE_THROTTLE: null, //触摸移动节流setTimeout
+				MOVE_THROTTLE_FLAG: true, //节流标识
+				TIME_CUT_CENTER: null,
+				CROPPER_WIDTH: 200, //裁剪框宽
+				CROPPER_HEIGHT: 200, //裁剪框高
+				CUT_START: null,
+				cutX: 0, //画布x轴起点
+				cutY: 0, //画布y轴起点0
+				touchRelative: [{
+					x: 0,
+					y: 0
+				}], //手指或鼠标和图片中心的相对位置
+				flagCutTouch: false, //是否是拖动裁剪框
+				hypotenuseLength: 0, //双指触摸时斜边长度
+				flagEndTouch: false, //是否结束触摸
+				canvasWidth: 0,
+				canvasHeight: 0,
+				imgWidth: 0, //图片宽度
+				imgHeight: 0, //图片高度
+				scale: 1, //图片缩放比
+				angle: 0, //图片旋转角度
+				cutAnimation: false, //是否开启图片和裁剪框过渡
+				cutAnimationTime: null,
+				imgTop: 0, //图片上边距
+				imgLeft: 0, //图片左边距
+				ctx: null,
+				sysInfo: null
+			};
+		},
+		computed: {
+			imgTransform: function() {
+				return `translate3d(${this.imgLeft - this.imgWidth / 2}px,${this.imgTop - this.imgHeight / 2}px,0) scale(${this.scale}) rotate(${this.angle}deg)`;
+			}
+		},
+		watch: {
+			imageUrl(val, oldVal) {
+				this.imageReset();
+				this.showLoading();
+				uni.getImageInfo({
+					src: val,
+					success: res => {
+						//计算图片尺寸
+						this.imgComputeSize(res.width, res.height);
+						if (this.limitMove) {
+							//限制移动,不留空白处理
+							this.imgMarginDetectionScale();
+						}
+					},
+					fail: err => {
+						this.imgComputeSize();
+						if (this.limitMove) {
+							this.imgMarginDetectionScale();
+						}
+					}
+				});
+			},
+			//监听截取框宽高变化
+			canvasWidth(val) {
+				if (val < this.minWidth) {
+					this.canvasWidth = this.minWidth;
+				}
+				this.computeCutSize();
+			},
+			canvasHeight(val) {
+				if (val < this.minHeight) {
+					this.canvasHeight = this.minHeight;
+				}
+				this.computeCutSize();
+			},
+			rotateAngle(val) {
+				this.cutAnimation = true;
+				this.angle = val;
+			},
+			angle(val) {
+				this.moveStop();
+				if (this.limitMove && val % 90) {
+					this.angle = Math.round(val / 90) * 90;
+				}
+				this.imgMarginDetectionScale();
+			},
+			cutAnimation(val) {
+				//开启过渡260毫秒之后自动关闭
+				clearTimeout(this.cutAnimationTime);
+				if (val) {
+					this.cutAnimationTime = setTimeout(() => {
+						this.cutAnimation = false;
+					}, 260);
+				}
+			},
+			limitMove(val) {
+				if (val) {
+					if (this.angle % 90) {
+						this.angle = Math.round(this.angle / 90) * 90;
+					}
+					this.imgMarginDetectionScale();
+				}
+			},
+			cutY(value) {
+				this.cutDetectionPosition();
+			},
+			cutX(value) {
+				this.cutDetectionPosition();
+			},
+			startCutting(val) {
+				if (this.custom && val) {
+					this.getImage();
+				}
+			}
+		},
+		mounted() {
+			this.sysInfo = uni.getSystemInfoSync();
+			this.imgTop = this.sysInfo.windowHeight / 2;
+			this.imgLeft = this.sysInfo.windowWidth / 2;
+			this.CROPPER_WIDTH = this.width;
+			this.CROPPER_HEIGHT = this.height;
+			this.canvasHeight = this.height;
+			this.canvasWidth = this.width;
+			this.ctx = uni.createCanvasContext('tui-image-cropper', this);
+			this.setCutCenter();
+			//设置裁剪框大小>设置图片尺寸>绘制canvas
+			this.computeCutSize();
+			//检查裁剪框是否在范围内
+			this.cutDetectionPosition();
+			setTimeout(() => {
+				this.$emit('ready', {});
+			}, 200);
+		},
+		methods: {
+			//网络图片转成本地文件[同步执行]
+			async getLocalImage(url) {
+				return await new Promise((resolve, reject) => {
+					uni.downloadFile({
+						url: url,
+						success: res => {
+							resolve(res.tempFilePath);
+						},
+						fail: res => {
+							reject(false)
+						}
+					})
+				})
+			},
+			//返回裁剪后图片信息
+			getImage() {
+				if (!this.imageUrl) {
+					uni.showToast({
+						title: '请选择图片',
+						icon: 'none'
+					});
+					return;
+				}
+				this.loadding && this.showLoading();
+				let draw = async () => {
+					//图片实际大小
+					let imgWidth = this.imgWidth * this.scale * this.scaleRatio;
+					let imgHeight = this.imgHeight * this.scale * this.scaleRatio;
+					//canvas和图片的相对距离
+					let xpos = this.imgLeft - this.cutX;
+					let ypos = this.imgTop - this.cutY;
+					//旋转画布
+					this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio);
+					this.ctx.rotate((this.angle * Math.PI) / 180);
+					let imgUrl = this.imageUrl;
+					// #ifdef APP-PLUS || MP-WEIXIN
+					if (~this.imageUrl.indexOf('https:')) {
+						imgUrl = await this.getLocalImage(this.imageUrl)
+					}
+					// #endif
+					this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
+					this.ctx.draw(false, () => {
+						let params = {
+							width: this.canvasWidth * this.scaleRatio,
+							height: Math.round(this.canvasHeight * this.scaleRatio),
+							destWidth: this.canvasWidth * this.scaleRatio,
+							destHeight: Math.round(this.canvasHeight) * this.scaleRatio,
+							fileType: 'png',
+							quality: this.quality
+						};
+						let data = {
+							url: '',
+							base64: '',
+							width: this.canvasWidth * this.scaleRatio,
+							height: this.canvasHeight * this.scaleRatio
+						};
+						// #ifdef MP-ALIPAY
+
+						if (this.isBase64) {
+							this.ctx.toDataURL(params).then(dataURL => {
+								data.base64 = dataURL;
+								this.loadding && uni.hideLoading();
+								this.$emit('cropper', data);
+							});
+						} else {
+							this.ctx.toTempFilePath({
+								...params,
+								success: res => {
+									data.url = res.tempFilePath;
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								}
+							});
+						}
+						// #endif
+
+						// #ifndef MP-ALIPAY
+						// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+						this.isBase64 = false;
+						// #endif
+						if (this.isBase64) {
+							uni.canvasGetImageData({
+								canvasId: 'tui-image-cropper',
+								x: 0,
+								y: 0,
+								width: this.canvasWidth * this.scaleRatio,
+								height: Math.round(this.canvasHeight * this.scaleRatio),
+								success: res => {
+									const arrayBuffer = new Uint8Array(res.data);
+									const base64 = uni.arrayBufferToBase64(arrayBuffer);
+									data.base64 = base64;
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								}
+							},this);
+						} else {
+							uni.canvasToTempFilePath({
+									...params,
+									canvasId: 'tui-image-cropper',
+									success: res => {
+										data.url = res.tempFilePath;
+										// #ifdef H5
+										data.base64 = res.tempFilePath;
+										// #endif
+										this.loadding && uni.hideLoading();
+										this.$emit('cropper', data);
+									},
+									fail(res) {
+										console.log(res);
+									}
+								},
+								this
+							);
+						}
+						// #endif
+					});
+				};
+				if (this.CROPPER_WIDTH != this.canvasWidth || this.CROPPER_HEIGHT != this.canvasHeight) {
+					this.CROPPER_WIDTH = this.canvasWidth;
+					this.CROPPER_HEIGHT = this.canvasHeight;
+					this.ctx.draw();
+					this.$nextTick(() => {
+						setTimeout(() => {
+							draw();
+						}, 100);
+					});
+				} else {
+					draw();
+				}
+			},
+			/**
+			 * 设置剪裁框和图片居中
+			 */
+			setCutCenter() {
+				let sys = this.sysInfo || uni.getSystemInfoSync();
+				let cutY = (sys.windowHeight - this.canvasHeight) * 0.5;
+				let cutX = (sys.windowWidth - this.canvasWidth) * 0.5;
+				//顺序不能变
+				this.imgTop = this.imgTop - this.cutY + cutY;
+				this.cutY = cutY; //截取的框上边距
+				this.imgLeft = this.imgLeft - this.cutX + cutX;
+				this.cutX = cutX; //截取的框左边距
+			},
+			imageReset() {
+				// this.cutAnimation = true;
+				this.scale = 1;
+				this.angle = 0;
+				let sys = this.sysInfo || uni.getSystemInfoSync();
+				this.imgTop = sys.windowHeight / 2;
+				this.imgLeft = sys.windowWidth / 2;
+			},
+			imageLoad(e) {
+				this.imageReset();
+				uni.hideLoading();
+				this.$emit('imageLoad', {});
+			},
+			//检测剪裁框位置是否在允许的范围内(屏幕内)
+			cutDetectionPosition() {
+				let cutDetectionPositionTop = () => {
+						//检测上边距是否在范围内
+						if (this.cutY < 0) {
+							this.cutY = 0;
+						}
+						if (this.cutY > this.sysInfo.windowHeight - this.canvasHeight) {
+							this.cutY = this.sysInfo.windowHeight - this.canvasHeight;
+						}
+					},
+					cutDetectionPositionLeft = () => {
+						//检测左边距是否在范围内
+						if (this.cutX < 0) {
+							this.cutX = 0;
+						}
+						if (this.cutX > this.sysInfo.windowWidth - this.canvasWidth) {
+							this.cutX = this.sysInfo.windowWidth - this.canvasWidth;
+						}
+					};
+				//裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中)
+				if (this.cutY == null && this.cutX == null) {
+					let cutY = (this.sysInfo.windowHeight - this.canvasHeight) * 0.5;
+					let cutX = (this.sysInfo.windowWidth - this.canvasWidth) * 0.5;
+					this.cutY = cutY; //截取的框上边距
+					this.cutX = cutX; //截取的框左边距
+				} else if (this.cutY != null && this.cutX != null) {
+					cutDetectionPositionTop();
+					cutDetectionPositionLeft();
+				} else if (this.cutY != null && this.cutX == null) {
+					cutDetectionPositionTop();
+					this.cutX = (this.sysInfo.windowWidth - this.canvasWidth) / 2;
+				} else if (this.cutY == null && this.cutX != null) {
+					cutDetectionPositionLeft();
+					this.cutY = (this.sysInfo.windowHeight - this.canvasHeight) / 2;
+				}
+			},
+			/**
+			 * 图片边缘检测-位置
+			 */
+			imgMarginDetectionPosition(scale) {
+				if (!this.limitMove) return;
+				let left = this.imgLeft;
+				let top = this.imgTop;
+				scale = scale || this.scale;
+				let imgWidth = this.imgWidth;
+				let imgHeight = this.imgHeight;
+				if ((this.angle / 90) % 2) {
+					imgWidth = this.imgHeight;
+					imgHeight = this.imgWidth;
+				}
+				left = this.cutX + (imgWidth * scale) / 2 >= left ? left : this.cutX + (imgWidth * scale) / 2;
+				left = this.cutX + this.canvasWidth - (imgWidth * scale) / 2 <= left ? left : this.cutX + this.canvasWidth - (
+					imgWidth * scale) / 2;
+				top = this.cutY + (imgHeight * scale) / 2 >= top ? top : this.cutY + (imgHeight * scale) / 2;
+				top = this.cutY + this.canvasHeight - (imgHeight * scale) / 2 <= top ? top : this.cutY + this.canvasHeight - (
+					imgHeight * scale) / 2;
+				this.imgLeft = left;
+				this.imgTop = top;
+				this.scale = scale;
+			},
+			/**
+			 * 图片边缘检测-缩放
+			 */
+			imgMarginDetectionScale(scale) {
+				if (!this.limitMove) return;
+				scale = scale || this.scale;
+				let imgWidth = this.imgWidth;
+				let imgHeight = this.imgHeight;
+				if ((this.angle / 90) % 2) {
+					imgWidth = this.imgHeight;
+					imgHeight = this.imgWidth;
+				}
+				if (imgWidth * scale < this.canvasWidth) {
+					scale = this.canvasWidth / imgWidth;
+				}
+				if (imgHeight * scale < this.canvasHeight) {
+					scale = Math.max(scale, this.canvasHeight / imgHeight);
+				}
+				this.imgMarginDetectionPosition(scale);
+			},
+			/**
+			 * 计算图片尺寸
+			 */
+			imgComputeSize(width, height) {
+				//默认按图片最小边 = 对应裁剪框尺寸
+				let imgWidth = width,
+					imgHeight = height;
+				if (imgWidth && imgHeight) {
+					if (imgWidth / imgHeight > (this.canvasWidth || this.width) / (this.canvasHeight || this.height)) {
+						imgHeight = this.canvasHeight || this.height;
+						imgWidth = (width / height) * imgHeight;
+					} else {
+						imgWidth = this.canvasWidth || this.width;
+						imgHeight = (height / width) * imgWidth;
+					}
+				} else {
+					let sys = this.sysInfo || uni.getSystemInfoSync();
+					imgWidth = sys.windowWidth;
+					imgHeight = 0;
+				}
+				this.imgWidth = imgWidth;
+				this.imgHeight = imgHeight;
+			},
+			//改变截取框大小
+			computeCutSize() {
+				if (this.canvasWidth > this.sysInfo.windowWidth) {
+					this.canvasWidth = this.sysInfo.windowWidth;
+				} else if (this.canvasWidth + this.cutX > this.sysInfo.windowWidth) {
+					this.cutX = this.sysInfo.windowWidth - this.cutX;
+				}
+				if (this.canvasHeight > this.sysInfo.windowHeight) {
+					this.canvasHeight = this.sysInfo.windowHeight;
+				} else if (this.canvasHeight + this.cutY > this.sysInfo.windowHeight) {
+					this.cutY = this.sysInfo.windowHeight - this.cutY;
+				}
+			},
+			//开始触摸
+			start(e) {
+				this.flagEndTouch = false;
+				if (e.touches.length == 1) {
+					//单指拖动
+					this.touchRelative[0] = {
+						x: e.touches[0].clientX - this.imgLeft,
+						y: e.touches[0].clientY - this.imgTop
+					};
+				} else {
+					//双指放大
+					let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
+					let height = Math.abs(e.touches[0].clientY - e.touches[1].clientY);
+					this.touchRelative = [{
+							x: e.touches[0].clientX - this.imgLeft,
+							y: e.touches[0].clientY - this.imgTop
+						},
+						{
+							x: e.touches[1].clientX - this.imgLeft,
+							y: e.touches[1].clientY - this.imgTop
+						}
+					];
+					this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+				}
+			},
+			moveThrottle() {
+				if (this.sysInfo.platform == 'android') {
+					clearTimeout(this.MOVE_THROTTLE);
+					this.MOVE_THROTTLE = setTimeout(() => {
+						this.MOVE_THROTTLE_FLAG = true;
+					}, 800 / 40);
+					return this.MOVE_THROTTLE_FLAG;
+				} else {
+					this.MOVE_THROTTLE_FLAG = true;
+				}
+			},
+			move(e) {
+				if (this.flagEndTouch || !this.MOVE_THROTTLE_FLAG) return;
+				this.MOVE_THROTTLE_FLAG = false;
+				this.moveThrottle();
+				this.moveDuring();
+				if (e.touches.length == 1) {
+					//单指拖动
+					let left = e.touches[0].clientX - this.touchRelative[0].x,
+						top = e.touches[0].clientY - this.touchRelative[0].y;
+					//图像边缘检测,防止截取到空白
+					this.imgLeft = left;
+					this.imgTop = top;
+					this.imgMarginDetectionPosition();
+				} else {
+					//双指放大
+					let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX),
+						height = Math.abs(e.touches[0].clientY - e.touches[1].clientY),
+						hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
+						scale = this.scale * (hypotenuse / this.hypotenuseLength),
+						current_deg = 0;
+					scale = scale <= this.minScale ? this.minScale : scale;
+					scale = scale >= this.maxScale ? this.maxScale : scale;
+					//图像边缘检测,防止截取到空白
+					// this.scale = scale;
+					this.imgMarginDetectionScale(scale);
+					//双指旋转(如果没禁用旋转)
+					let touchRelative = [{
+							x: e.touches[0].clientX - this.imgLeft,
+							y: e.touches[0].clientY - this.imgTop
+						},
+						{
+							x: e.touches[1].clientX - this.imgLeft,
+							y: e.touches[1].clientY - this.imgTop
+						}
+					];
+					if (!this.disableRotate) {
+						let first_atan = (180 / Math.PI) * Math.atan2(touchRelative[0].y, touchRelative[0].x);
+						let first_atan_old = (180 / Math.PI) * Math.atan2(this.touchRelative[0].y, this.touchRelative[0].x);
+						let second_atan = (180 / Math.PI) * Math.atan2(touchRelative[1].y, touchRelative[1].x);
+						let second_atan_old = (180 / Math.PI) * Math.atan2(this.touchRelative[1].y, this.touchRelative[1].x);
+						//当前旋转的角度
+						let first_deg = first_atan - first_atan_old,
+							second_deg = second_atan - second_atan_old;
+						if (first_deg != 0) {
+							current_deg = first_deg;
+						} else if (second_deg != 0) {
+							current_deg = second_deg;
+						}
+					}
+					this.touchRelative = touchRelative;
+					this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+					//更新视图
+					this.angle = this.angle + current_deg;
+					this.scale = this.scale;
+				}
+			},
+			//结束操作
+			end(e) {
+				this.flagEndTouch = true;
+				this.moveStop();
+			},
+			//裁剪框处理
+			cutTouchMove(e) {
+				if (this.flagCutTouch && this.MOVE_THROTTLE_FLAG) {
+					if (this.lockRatio && (this.lockWidth || this.lockHeight)) return;
+					//节流
+					this.MOVE_THROTTLE_FLAG = false;
+					this.moveThrottle();
+					let width = this.canvasWidth,
+						height = this.canvasHeight,
+						cutY = this.cutY,
+						cutX = this.cutX,
+						size_correct = () => {
+							width = width <= this.maxWidth ? (width >= this.minWidth ? width : this.minWidth) : this.maxWidth;
+							height = height <= this.maxHeight ? (height >= this.minHeight ? height : this.minHeight) : this.maxHeight;
+						},
+						size_inspect = () => {
+							if ((width > this.maxWidth || width < this.minWidth || height > this.maxHeight || height < this.minHeight) &&
+								this.lockRatio) {
+								size_correct();
+								return false;
+							} else {
+								size_correct();
+								return true;
+							}
+						};
+					height = this.CUT_START.height + (this.CUT_START.corner > 1 && this.CUT_START.corner < 4 ? 1 : -1) * (this.CUT_START
+						.y - e.touches[0].clientY);
+					switch (this.CUT_START.corner) {
+						case 1:
+							width = this.CUT_START.width - this.CUT_START.x + e.touches[0].clientX;
+							if (this.lockRatio) {
+								height = width / (this.canvasWidth / this.canvasHeight);
+							}
+							if (!size_inspect()) return;
+							break;
+						case 2:
+							width = this.CUT_START.width - this.CUT_START.x + e.touches[0].clientX;
+							if (this.lockRatio) {
+								height = width / (this.canvasWidth / this.canvasHeight);
+							}
+							if (!size_inspect()) return;
+							cutY = this.CUT_START.cutY - (height - this.CUT_START.height);
+							break;
+						case 3:
+							width = this.CUT_START.width + this.CUT_START.x - e.touches[0].clientX;
+							if (this.lockRatio) {
+								height = width / (this.canvasWidth / this.canvasHeight);
+							}
+							if (!size_inspect()) return;
+							cutY = this.CUT_START.cutY - (height - this.CUT_START.height);
+							cutX = this.CUT_START.cutX - (width - this.CUT_START.width);
+							break;
+						case 4:
+							width = this.CUT_START.width + this.CUT_START.x - e.touches[0].clientX;
+							if (this.lockRatio) {
+								height = width / (this.canvasWidth / this.canvasHeight);
+							}
+							if (!size_inspect()) return;
+							cutX = this.CUT_START.cutX - (width - this.CUT_START.width);
+							break;
+						default:
+							break;
+					}
+					if (!this.lockWidth && !this.lockHeight) {
+						this.canvasWidth = width;
+						this.cutX = cutX;
+						this.canvasHeight = height;
+						this.cutY = cutY;
+					} else if (!this.lockWidth) {
+						this.canvasWidth = width;
+						this.cutX = cutX;
+					} else if (!this.lockHeight) {
+						this.canvasHeight = height;
+						this.cutY = cutY;
+					}
+					this.imgMarginDetectionScale();
+				}
+			},
+			cutTouchStart(e) {
+				let currentX = e.touches[0].clientX;
+				let currentY = e.touches[0].clientY;
+
+				/*
+				 * (右下-1 右上-2 左上-3 左下-4)
+				 * left_x [3,4]
+				 * top_y [2,3]
+				 * right_x [1,2]
+				 * bottom_y [1,4]
+				 */
+				let left_x1 = this.cutX - 24;
+				let left_x2 = this.cutX + 24;
+
+				let top_y1 = this.cutY - 24;
+				let top_y2 = this.cutY + 24;
+
+				let right_x1 = this.cutX + this.canvasWidth - 24;
+				let right_x2 = this.cutX + this.canvasWidth + 24;
+
+				let bottom_y1 = this.cutY + this.canvasHeight - 24;
+				let bottom_y2 = this.cutY + this.canvasHeight + 24;
+
+				if (currentX > right_x1 && currentX < right_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
+					this.moveDuring();
+					this.flagCutTouch = true;
+					this.flagEndTouch = true;
+					this.CUT_START = {
+						width: this.canvasWidth,
+						height: this.canvasHeight,
+						x: currentX,
+						y: currentY,
+						corner: 1
+					};
+				} else if (currentX > right_x1 && currentX < right_x2 && currentY > top_y1 && currentY < top_y2) {
+					this.moveDuring();
+					this.flagCutTouch = true;
+					this.flagEndTouch = true;
+					this.CUT_START = {
+						width: this.canvasWidth,
+						height: this.canvasHeight,
+						x: currentX,
+						y: currentY,
+						cutY: this.cutY,
+						cutX: this.cutX,
+						corner: 2
+					};
+				} else if (currentX > left_x1 && currentX < left_x2 && currentY > top_y1 && currentY < top_y2) {
+					this.moveDuring();
+					this.flagCutTouch = true;
+					this.flagEndTouch = true;
+					this.CUT_START = {
+						width: this.canvasWidth,
+						height: this.canvasHeight,
+						cutY: this.cutY,
+						cutX: this.cutX,
+						x: currentX,
+						y: currentY,
+						corner: 3
+					};
+				} else if (currentX > left_x1 && currentX < left_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
+					this.moveDuring();
+					this.flagCutTouch = true;
+					this.flagEndTouch = true;
+					this.CUT_START = {
+						width: this.canvasWidth,
+						height: this.canvasHeight,
+						cutY: this.cutY,
+						cutX: this.cutX,
+						x: currentX,
+						y: currentY,
+						corner: 4
+					};
+				}
+			},
+			cutTouchEnd(e) {
+				this.moveStop();
+				this.flagCutTouch = false;
+			},
+			//停止移动时需要做的操作
+			moveStop() {
+				//清空之前的自动居中延迟函数并添加最新的
+				clearTimeout(this.TIME_CUT_CENTER);
+				this.TIME_CUT_CENTER = setTimeout(() => {
+					//动画启动
+					if (!this.cutAnimation) {
+						this.cutAnimation = true;
+					}
+					this.setCutCenter();
+				}, 800);
+			},
+			//移动中
+			moveDuring() {
+				//清空之前的自动居中延迟函数
+				clearTimeout(this.TIME_CUT_CENTER);
+			},
+			showLoading() {
+				uni.showLoading({
+					title: '请稍候...',
+					mask: true
+				});
+			},
+			stop() {},
+			back() {
+				uni.navigateBack();
+			},
+			setAngle() {
+				this.cutAnimation = true;
+				this.angle = this.angle + 90;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-container {
+		width: 100vw;
+		height: 100vh;
+		background-color: rgba(0, 0, 0, 0.6);
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 1;
+	}
+
+	.tui-image-cropper {
+		width: 100vw;
+		height: 100vh;
+		position: absolute;
+	}
+
+	.tui-content {
+		width: 100vw;
+		height: 100vh;
+		position: absolute;
+		z-index: 9;
+		display: flex;
+		flex-direction: column;
+		pointer-events: none;
+	}
+
+	.tui-bg-transparent {
+		background-color: rgba(0, 0, 0, 0.6);
+		transition-duration: 0.35s;
+	}
+
+	.tui-content-top {
+		pointer-events: none;
+	}
+
+	.tui-content-middle {
+		width: 100%;
+		height: 200px;
+		display: flex;
+		box-sizing: border-box;
+	}
+
+	.tui-cropper-box {
+		position: relative;
+		/* transition-duration: 0.3s; */
+		border-style: solid;
+		border-width: 1rpx;
+		box-sizing: border-box;
+	}
+
+	.tui-flex-auto {
+		flex: auto;
+	}
+
+	.tui-cropper-image {
+		width: 100%;
+		border-style: none;
+		position: absolute;
+		top: 0;
+		left: 0;
+		z-index: 2;
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		transform-origin: center;
+	}
+
+	.tui-cropper-canvas {
+		position: fixed;
+		z-index: 10;
+		left: -2000px;
+		top: -2000px;
+		pointer-events: none;
+	}
+
+	.tui-edge {
+		border-style: solid;
+		pointer-events: auto;
+		position: absolute;
+		box-sizing: border-box;
+	}
+
+	.tui-top-left {
+		border-bottom-width: 0 !important;
+		border-right-width: 0 !important;
+	}
+
+	.tui-top-right {
+		border-bottom-width: 0 !important;
+		border-left-width: 0 !important;
+	}
+
+	.tui-bottom-left {
+		border-top-width: 0 !important;
+		border-right-width: 0 !important;
+	}
+
+	.tui-bottom-right {
+		border-top-width: 0 !important;
+		border-left-width: 0 !important;
+	}
+
+	.tui-cropper-tabbar {
+		width: 100%;
+		height: 120rpx;
+		padding: 0 40rpx;
+		box-sizing: border-box;
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		z-index: 99;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		color: #ffffff;
+		font-size: 32rpx;
+	}
+
+	.tui-cropper-tabbar::after {
+		content: ' ';
+		position: absolute;
+		top: 0;
+		right: 0;
+		left: 0;
+		border-top: 1rpx solid rgba(255, 255, 255, 0.2);
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 100%;
+	}
+
+	.tui-op-btn {
+		height: 80rpx;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-rotate-img {
+		width: 44rpx;
+		height: 44rpx;
+	}
+</style>

+ 163 - 0
components/thorui/tui-image-group/tui-image-group.vue

@@ -0,0 +1,163 @@
+<template>
+	<view
+		class="tui-image-container"
+		:style="{ marginBottom: multiLine ? `-${distance}rpx` : 0 }"
+		:class="{ 'tui-image-direction': direction == 'column', 'tui-image__warp': multiLine }"
+	>
+		<view
+			v-for="(item, index) in imageList"
+			:key="index"
+			class="tui-image__itembox"
+			:style="{
+				width: width,
+				height: height,
+				borderRadius: radius,
+				marginLeft: direction == 'column' || multiLine ? 0 : (index && distance) + 'rpx',
+				marginRight: multiLine ? distance + 'rpx' : 0,
+				marginBottom: multiLine ? distance + 'rpx' : 0,
+				marginTop: direction == 'row' ? 0 : (index && distance) + 'rpx'
+			}"
+			@tap="bindClick(index, item.id)"
+		>
+			<image
+				class="tui-image-item"
+				:mode="mode"
+				:lazy-load="lazyLoad"
+				fade-show="fadeShow"
+				:webp="webp"
+				:show-menu-by-longpress="longpress"
+				@error="error"
+				@load="load"
+				:style="{ width: width, height: height, borderRadius: radius, borderWidth: borderWidth, borderColor: borderColor }"
+				:src="item.src"
+			></image>
+			<slot />
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiImageGroup',
+	props: {
+		//图片集合
+		/*
+		  [{id:1,src:"1.png"}]
+		*/
+		imageList: {
+			type: Array,
+			default: () => {
+				return [];
+			}
+		},
+		//图片宽度
+		width: {
+			type: String,
+			default: '120rpx'
+		},
+		//图片高度
+		height: {
+			type: String,
+			default: '120rpx'
+		},
+		//图片边框宽度 rpx
+		borderWidth: {
+			type: String,
+			default: '0'
+		},
+		//图片边框颜色 可传rgba
+		borderColor: {
+			type: String,
+			default: '#fff'
+		},
+		//图片圆角
+		radius: {
+			type: String,
+			default: '50%'
+		},
+		//图片裁剪、缩放的模式
+		mode: {
+			type: String,
+			default: 'scaleToFill'
+		},
+		//图片懒加载。只针对page与scroll-view下的image有效
+		lazyLoad: {
+			type: Boolean,
+			default: true
+		},
+		//图片显示动画效果 | 仅App-nvue 2.3.4+ Android有效
+		fadeShow: {
+			type: Boolean,
+			default: true
+		},
+		//默认不解析 webP 格式,只支持网络资源 | 微信小程序2.9.0
+		webp: {
+			type: Boolean,
+			default: false
+		},
+		//开启长按图片显示识别小程序码菜单 | 微信小程序2.7.0
+		longpress: {
+			type: Boolean,
+			default: false
+		},
+		//是否组合排列
+		isGroup: {
+			type: Boolean,
+			default: false
+		},
+		//排列方向 row ,column
+		direction: {
+			type: String,
+			default: 'row'
+		},
+		//偏移距离 rpx
+		distance: {
+			type: [Number, String],
+			default: -16
+		},
+		//是否可多行展示,排列方向 row时生效,distance需设置为大于0的数
+		multiLine: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		error(e) {
+			this.$emit('errorEvent', e);
+		},
+		load(e) {
+			this.$emit('loaded', e);
+		},
+		bindClick(index, id) {
+			this.$emit('click', {
+				index: index,
+				id: id || ''
+			});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-image-container {
+	display: inline-flex;
+	align-items: center;
+}
+.tui-image-direction {
+	flex-direction: column;
+}
+.tui-image__warp {
+	flex-wrap: wrap;
+}
+.tui-image__itembox {
+	position: relative;
+}
+.tui-image-item {
+	border-style: solid;
+	flex-shrink: 0;
+	display: block;
+}
+</style>

+ 73 - 0
components/thorui/tui-keyboard-input/tui-keyboard-input.vue

@@ -0,0 +1,73 @@
+<template>
+	<view class="tui-keyboard-input tui-pwd-box" :style="{backgroundColor:backgroundColor}">
+		<view class="tui-inner-box">
+			<view class="tui-input" :class="[inputvalue.length===4?'tui-margin-right':'']" :style="{fontSize:size+'rpx',color:color,width:(inputvalue.length===4?90:70)+'rpx' }"
+			 v-for="(item,index) in inputvalue" :key="index">{{item}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiKeyboardInput",
+		props: {
+			//背景颜色
+			backgroundColor: {
+				type: String,
+				default: "#fff"
+			},
+			size: {
+				type: Number,
+				default: 32
+			},
+			color: {
+				type: String,
+				default: "#333"
+			},
+			//输入框的值:数组格式,长度即为输入框个数
+			inputvalue: {
+				type: Array,
+				default: ["", "", "", "", "", ""] //密码圆点 ●
+			}
+		},
+		data() {
+			return {
+
+			};
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-pwd-box {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		box-sizing: border-box;
+		vertical-align: top;
+	}
+
+	.tui-inner-box {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-input {
+		height: 80rpx;
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		margin-right: 20rpx;
+		border-bottom: 2px solid #666;
+	}
+
+	.tui-margin-right {
+		margin-right: 30rpx;
+	}
+
+	.tui-input:last-child {
+		margin-right: 0 !important;
+	}
+</style>

+ 240 - 0
components/thorui/tui-keyboard/tui-keyboard.vue

@@ -0,0 +1,240 @@
+<template>
+	<view>
+		<view class="tui-keyboard-mask" :class="[show?'tui-mask-show':'']" v-if="mask" @tap="handleClose"></view>
+		<view class="tui-keyboard" :class="{'tui-keyboard-radius':radius,'tui-keyboard-action':action,'tui-keyboard-show':show}">
+			<slot></slot>
+			<view class="tui-keyboard-grids">
+				<!--{{(index==9 || index==10 || index==11)?'tui-grid-bottom':''}}-->
+				<view class="tui-keyboard-grid" :class="{'tui-bg-gray':index==9 || index==11}" v-for="(item,index) in itemList"
+				 :key="index" hover-class="tui-keyboard-hover" :hover-stay-time="150" @tap="handleClick" :data-index="index">
+					<view v-if="index<11" class="tui-keyboard-item" :class="{'tui-fontsize-32':index==9}">{{getKeyBoard(index,action)}}</view>
+					<view v-else class="tui-keyboard-item">
+						<view class="tui-icon tui-keyboard-delete"></view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiKeyboard",
+		props: {
+			//是否需要mask
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			//控制键盘显示
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//是否直接显示,不需要动画,一般使用在锁屏密码
+			action: {
+				type: Boolean,
+				default: true
+			},
+			//是否带圆角
+			radius: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				itemList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+			};
+		},
+		methods: {
+			getKeyBoard: function(index, action) {
+				var content = index + 1;
+				if (index == 9) {
+					content = action ? "取消" : "清除";
+				} else if (index == 10) {
+					content = 0;
+				}
+				return content;
+			},
+			//关闭
+			handleClose() {
+				if (!this.show) {
+					return;
+				}
+				this.$emit('close', {});
+			},
+			handleClick(e) {
+				if (!this.show) {
+					return;
+				}
+				const dataset = e.currentTarget.dataset;
+				this.$emit('click', {
+					index: Number(dataset.index)
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'keyboardFont';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAASgAA0AAAAABugAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEhAAAABoAAAAch/nJvUdERUYAAARkAAAAHgAAAB4AKQAKT1MvMgAAAZwAAABDAAAAVj4mSapjbWFwAAAB8AAAAD4AAAFCAA/rY2dhc3AAAARcAAAACAAAAAj//wADZ2x5ZgAAAjwAAACsAAAA0BLVU2FoZWFkAAABMAAAAC0AAAA2FXPmsWhoZWEAAAFgAAAAHAAAACQH3gOFaG10eAAAAeAAAAAOAAAAEAwAAABsb2NhAAACMAAAAAoAAAAKAGgAAG1heHAAAAF8AAAAHwAAACABEQBLbmFtZQAAAugAAAFJAAACiCnmEVVwb3N0AAAENAAAACgAAAA6nLlLs3jaY2BkYGAAYukqK754fpuvDNwsDCBwU+tiFBKtwMLA9ABIczAwgUQB4ccH+gAAAHjaY2BkYGBu+N/AEMPCAAJAkpEBFbAAAEcKAm142mNgZGBgYGGwZ2BmAAEmIOYCQgaG/2A+AwAPIgFdAHjaY2BkYWCcwMDKwMDUyXSGgYGhH0IzvmYwYuQAijKwMjNgBQFprikMDs93PN/B3PC/gSGGuYGhASjMCJIDAPenDU0AeNpjYYAAFigGAACAAA0AAHjaY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+8x3//0NICW+oSgZGNgYYk4GRCUgwMaACRoZhDwAItAhZAAAAAAAAAAAAAABoAAB42l3MTQqCUBSG4fNpqBxECS/+YFTXRGcFKteZjW0nuoqWVtOgPbgKZ1cqaBDN3snzkklE+xUZEwUkqSOCzGx4EGGEsJYd2vURgQdbomhayC0iu8h8lEVmiR1sS4TVGVFYqeaEVjXmVT8TsWjf83yYIjFq1QM9I0/1c9HMMI06zfHgmMeRY8HDwOKnjSlYZvdQ5u4yB+gVbqrX97cAOxsHn9GF/9G3iV4WbSWBeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMCiTIxM/FmZiXkFiXnxxRmJeckZpQA1nQZRAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMAAwABAAQAAAACAAAAAHjaY2BgYGQAgqtL1DlA9E2ti1EwGgA9dwYGAAA=) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-icon {
+		font-family: "keyboardFont" !important;
+		font-size: 22px;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+		line-height: 1;
+		color: #333;
+	}
+
+	.tui-keyboard-delete:before {
+		content: "\e7b8";
+	}
+
+	.tui-keyboard-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.6);
+		z-index: 998;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+
+	.tui-keyboard {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 999;
+		padding-bottom: env(safe-area-inset-bottom);
+		background-color: #fff;
+	}
+
+	.tui-keyboard-radius {
+		border-top-left-radius: 16rpx;
+		border-top-right-radius: 16rpx;
+		overflow: hidden;
+	}
+
+	.tui-keyboard-action {
+		visibility: hidden;
+		transform: translate3d(0, 100%, 0);
+		transform-origin: center;
+		transition: all 0.3s ease-in-out;
+	}
+
+	.tui-keyboard-show {
+		transform: translate3d(0, 0, 0);
+		visibility: visible;
+	}
+
+	.tui-bg-gray {
+		background-color: #e7e6eb !important;
+	}
+
+	.tui-keyboard-grids {
+		width: 100%;
+		position: relative;
+		overflow: hidden;
+		display: flex;
+		display: -webkit-flex;
+		flex-direction: row;
+		flex-wrap: wrap;
+	}
+
+	.tui-keyboard-grids::after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 1px;
+		border-top: 1px solid #eaeef1;
+		-webkit-transform-origin: 0 0;
+		transform-origin: 0 0;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+	}
+
+	.tui-keyboard-grid {
+		position: relative;
+		padding: 24rpx 20rpx;
+		box-sizing: border-box;
+		background-color: #fff;
+		width: 33.33333333%;
+	}
+
+	.tui-keyboard-grid:nth-of-type(3n)::before {
+		width: 0;
+		border-right: 0;
+	}
+
+	.tui-keyboard-grid::before {
+		content: " ";
+		position: absolute;
+		right: 0;
+		top: 0;
+		width: 1px;
+		bottom: 0;
+		border-right: 1px solid #eaeef1;
+		-webkit-transform-origin: 100% 0;
+		transform-origin: 100% 0;
+		-webkit-transform: scaleX(0.5);
+		transform: scaleX(0.5);
+	}
+
+	.tui-keyboard-grid::after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		height: 1px;
+		border-bottom: 1px solid #eaeef1;
+		-webkit-transform-origin: 0 100%;
+		transform-origin: 0 100%;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+	}
+
+	.tui-grid-bottom::after {
+		height: 0 !important;
+		border-bottom: 0 !important;
+	}
+
+	.tui-keyboard-hover {
+		background-color: #f7f7f9 !important;
+	}
+
+	.tui-keyboard-item {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 48rpx;
+		height: 60rpx;
+		color: #000;
+	}
+
+	.tui-fontsize-32 {
+		font-size: 32rpx;
+		color: #333 !important;
+	}
+</style>

+ 172 - 0
components/thorui/tui-list-cell/tui-list-cell.vue

@@ -0,0 +1,172 @@
+<template>
+	<view
+		class="tui-list-class tui-list-cell"
+		:class="[
+			arrow ? 'tui-cell-arrow' : '',
+			arrow && arrowRight ? '' : 'tui-arrow-right',
+			unlined ? 'tui-cell-unlined' : '',
+			lineLeft ? 'tui-line-left' : '',
+			lineRight ? 'tui-line-right' : '',
+			arrow && arrowColor ? 'tui-arrow-' + arrowColor : '',
+			radius ? 'tui-radius' : ''
+		]"
+		:hover-class="hover ? 'tui-cell-hover' : ''"
+		:style="{ backgroundColor: backgroundColor, fontSize: size + 'rpx', color: color, padding: padding }"
+		:hover-stay-time="150"
+		@tap="handleClick"
+	>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiListCell',
+	props: {
+		//是否有箭头
+		arrow: {
+			type: Boolean,
+			default: false
+		},
+		//箭头颜色 传值: white,gray,warning,danger
+		arrowColor: {
+			type: String,
+			default: ''
+		},
+		//是否有点击效果
+		hover: {
+			type: Boolean,
+			default: true
+		},
+		//隐藏线条
+		unlined: {
+			type: Boolean,
+			default: false
+		},
+		//线条是否有左偏移距离
+		lineLeft: {
+			type: Boolean,
+			default: true
+		},
+		//线条是否有右偏移距离
+		lineRight: {
+			type: Boolean,
+			default: false
+		},
+		padding: {
+			type: String,
+			default: '26rpx 30rpx'
+		},
+		//背景颜色
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		},
+		//字体大小
+		size: {
+			type: Number,
+			default: 28
+		},
+		//字体颜色
+		color: {
+			type: String,
+			default: '#333'
+		},
+		//是否加圆角
+		radius: {
+			type: Boolean,
+			default: false
+		},
+		//箭头是否有偏移距离
+		arrowRight: {
+			type: Boolean,
+			default: true
+		},
+		index: {
+			type: Number,
+			default: 0
+		}
+	},
+	methods: {
+		handleClick() {
+			this.$emit('click', {
+				index: this.index
+			});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-list-cell {
+	position: relative;
+	width: 100%;
+	box-sizing: border-box;
+}
+.tui-radius {
+	border-radius: 6rpx;
+	overflow: hidden;
+}
+
+.tui-cell-hover {
+	background-color: #f1f1f1 !important;
+}
+
+.tui-list-cell::after {
+	content: '';
+	position: absolute;
+	border-bottom: 1px solid #eaeef1;
+	-webkit-transform: scaleY(0.5) translateZ(0);
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 100%;
+	bottom: 0;
+	right: 0;
+	left: 0;
+	pointer-events: none;
+}
+
+.tui-line-left::after {
+	left: 30rpx !important;
+}
+
+.tui-line-right::after {
+	right: 30rpx !important;
+}
+
+.tui-cell-unlined::after {
+	border-bottom: 0 !important;
+}
+
+.tui-cell-arrow::before {
+	content: ' ';
+	height: 10px;
+	width: 10px;
+	border-width: 2px 2px 0 0;
+	border-color: #c0c0c0;
+	border-style: solid;
+	-webkit-transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
+	transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
+	position: absolute;
+	top: 50%;
+	margin-top: -6px;
+	right: 30rpx;
+}
+.tui-arrow-right::before {
+	right: 0 !important;
+}
+.tui-arrow-gray::before {
+	border-color: #666666 !important;
+}
+.tui-arrow-white::before {
+	border-color: #ffffff !important;
+}
+.tui-arrow-warning::before {
+	border-color: #ff7900 !important;
+}
+.tui-arrow-success::before {
+	border-color: #19be6b !important;
+}
+.tui-arrow-danger::before {
+	border-color: #eb0909 !important;
+}
+</style>

+ 97 - 0
components/thorui/tui-list-view/tui-list-view.vue

@@ -0,0 +1,97 @@
+<template>
+	<view class="tui-list-view tui-view-class" :style="{backgroundColor:backgroundColor,marginTop:marginTop}">
+		<view class="tui-list-title" :style="{color:color,fontSize:size+'rpx',lineHeight:30+'rpx'}" v-if="title">{{title}}</view>
+		<view class="tui-list-content" :class="[unlined?'tui-border-'+unlined:'']">
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiListView",
+		props: {
+			title: {
+				type: String,
+				default: ''
+			},
+			color:{
+				type: String,
+				default: '#666'
+			},
+			//rpx
+			size:{
+				type:Number,
+				default:30
+			},
+			backgroundColor:{
+				type: String,
+				default: 'transparent'
+			},
+			unlined: {
+				type: String,
+				default: '' //top,bottom,all
+			},
+			marginTop:{
+				type:String,
+				default:'0'
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-list-title {
+		width: 100%;
+		padding: 30rpx;
+		box-sizing: border-box;
+	}
+
+	.tui-list-content {
+		width: 100%;
+		position: relative;
+	}
+
+	.tui-list-content::before {
+		content: " ";
+		position: absolute;
+		top: 0;
+		right: 0;
+		left: 0;
+		border-top: 1px solid #eaeef1;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+	.tui-list-content::after {
+		content: '';
+		width: 100%;
+		position: absolute;
+		border-bottom: 1px solid #eaeef1;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 100%;
+		bottom: 0;
+		right: 0;
+		left: 0;
+	}
+
+	.tui-border-top::before {
+		border-top: 0;
+	}
+
+	.tui-border-bottom::after {
+		border-bottom: 0;
+	}
+
+	.tui-border-all::after {
+		border-bottom: 0;
+	}
+
+	.tui-border-all::before {
+		border-top: 0;
+	}
+</style>

+ 78 - 0
components/thorui/tui-loading/tui-loading.vue

@@ -0,0 +1,78 @@
+<template>
+	<view class="tui-loading-init">
+		<view class="tui-loading-center"></view>
+		<view class="tui-loadmore-tips">{{text}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiLoading",
+		props: {
+			text: {
+				type: String,
+				default: "正在加载..."
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-loading-init {
+		min-width: 200rpx;
+		min-height: 200rpx;
+		max-width: 500rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		position: fixed;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		z-index: 9999;
+		font-size: 26rpx;
+		color: #fff;
+		background-color: rgba(0, 0, 0, 0.7);
+		border-radius: 10rpx;
+	}
+
+	.tui-loading-center {
+		width: 50rpx;
+		height: 50rpx;
+		border: 3px solid #fff;
+		border-radius: 50%;
+		margin: 0 6px;
+		display: inline-block;
+		vertical-align: middle;
+		clip-path: polygon(0% 0%, 100% 0%, 100% 40%, 0% 40%);
+		animation: rotate 1s linear infinite;
+		margin-bottom: 36rpx;
+	}
+
+	.tui-loadmore-tips {
+		text-align: center;
+		padding: 0 20rpx;
+		box-sizing: border-box;
+	}
+
+	@-webkit-keyframes rotate {
+		from {
+			transform: rotatez(0deg);
+		}
+
+		to {
+			transform: rotatez(360deg);
+		}
+	}
+
+	@keyframes rotate {
+		from {
+			transform: rotatez(0deg);
+		}
+
+		to {
+			transform: rotatez(360deg);
+		}
+	}
+</style>

+ 161 - 0
components/thorui/tui-loadmore/tui-loadmore.vue

@@ -0,0 +1,161 @@
+<template>
+	<view class="tui-loadmore">
+		<view :class="['tui-loading-'+index, (index==3 && type)?'tui-loading-'+type:'']"></view>
+		<view class="tui-loadmore-tips">{{text}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiLoadmore",
+		props: {
+			//显示文本
+			text: {
+				type: String,
+				default: "正在加载..."
+			},
+			//loading 样式 :1,2,3
+			index: {
+				type: Number,
+				default: 1
+			},
+			//颜色设置,只有index=3时生效:primary,red,orange,green
+			type: {
+				type: String,
+				default: ""
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-loadmore {
+		width: 48%;
+		margin: 1.5em auto;
+		line-height: 1.5em;
+		font-size: 24rpx;
+		text-align: center;
+	}
+
+	.tui-loading-1 {
+		margin: 0 5px;
+		width: 20px;
+		height: 20px;
+		display: inline-block;
+		vertical-align: middle;
+		-webkit-animation: a 1s steps(12) infinite;
+		animation: a 1s steps(12) infinite;
+		background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
+		background-size: 100%;
+	}
+
+	@-webkit-keyframes a {
+		0% {
+			-webkit-transform: rotate(0deg);
+			transform: rotate(0deg);
+		}
+
+		to {
+			-webkit-transform: rotate(1turn);
+			transform: rotate(1turn);
+		}
+	}
+
+	@keyframes a {
+		0% {
+			-webkit-transform: rotate(0deg);
+			transform: rotate(0deg);
+		}
+
+		to {
+			-webkit-transform: rotate(1turn);
+			transform: rotate(1turn);
+		}
+	}
+
+	.tui-loadmore-tips {
+		display: inline-block;
+		vertical-align: middle;
+	}
+
+	.tui-loading-2 {
+		width: 28rpx;
+		height: 28rpx;
+		border: 1px solid #8f8d8e;
+		border-radius: 50%;
+		margin: 0 6px;
+		display: inline-block;
+		vertical-align: middle;
+		clip-path: polygon(0% 0%,100% 0%,100% 30%,0% 30%);
+		animation: rotate 1s linear infinite;
+	}
+
+	@-webkit-keyframes rotate {
+		from {
+			transform: rotatez(0deg);
+		}
+
+		to {
+			transform: rotatez(360deg);
+		}
+	}
+
+	@keyframes rotate {
+		from {
+			transform: rotatez(0deg);
+		}
+
+		to {
+			transform: rotatez(360deg);
+		}
+	}
+
+	.tui-loading-3 {
+		display: inline-block;
+		margin: 0 6px;
+		vertical-align: middle;
+		width: 28rpx;
+		height: 28rpx;
+		background: 0 0;
+		border-radius: 50%;
+		border: 2px solid;
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
+		animation: tui-rotate 0.7s linear infinite;
+	}
+
+	.tui-loading-3.tui-loading-primary {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #5677fc;
+	}
+
+	.tui-loading-3.tui-loading-green {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #19be6b;
+	}
+
+	.tui-loading-3.tui-loading-orange {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #ff7900;
+	}
+
+	.tui-loading-3.tui-loading-red {
+		border-color: #ededed #ededed #ededed #ed3f14;
+	}
+
+	@-webkit-keyframes tui-rotate {
+		0% {
+			transform: rotate(0);
+		}
+
+		100% {
+			transform: rotate(360deg);
+		}
+	}
+
+	@keyframes tui-rotate {
+		0% {
+			transform: rotate(0);
+		}
+
+		100% {
+			transform: rotate(360deg);
+		}
+	}
+</style>

+ 407 - 0
components/thorui/tui-modal/tui-modal.vue

@@ -0,0 +1,407 @@
+<template>
+	<view class="tui-modal__container" :class="[show ? 'tui-modal-show' : '']" :style="{zIndex:zIndex}" @touchmove.stop.prevent>
+		<view
+			class="tui-modal-box"
+			:style="{ width: width, padding: padding, borderRadius: radius, backgroundColor: backgroundColor,zIndex:zIndex+1 }"
+			:class="[fadeIn || show ? 'tui-modal-normal' : 'tui-modal-scale', show ? 'tui-modal-show' : '']"
+		>
+			<view v-if="!custom">
+				<view class="tui-modal-title" v-if="title">{{ title }}</view>
+				<view class="tui-modal-content" :class="[title ? '' : 'tui-mtop']" :style="{ color: color, fontSize: size + 'rpx' }">{{ content }}</view>
+				<view class="tui-modalBtn-box" :class="[button.length != 2 ? 'tui-flex-column' : '']">
+					<block v-for="(item, index) in button" :key="index">
+						<button
+							class="tui-modal-btn"
+							:class="[
+								'tui-' + (item.type || 'primary') + (item.plain ? '-outline' : ''),
+								button.length != 2 ? 'tui-btn-width' : '',
+								button.length > 2 ? 'tui-mbtm' : '',
+								shape == 'circle' ? 'tui-circle-btn' : ''
+							]"
+							:hover-class="'tui-' + (item.plain ? 'outline' : item.type || 'primary') + '-hover'"
+							:data-index="index"
+							@tap="handleClick"
+						>
+							{{ item.text || '确定' }}
+						</button>
+					</block>
+				</view>
+			</view>
+			<view v-else><slot></slot></view>
+		</view>
+		<view class="tui-modal-mask" :class="[show ? 'tui-mask-show' : '']" :style="{zIndex:maskZIndex}" @tap="handleClickCancel"></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiModal',
+	props: {
+		//是否显示
+		show: {
+			type: Boolean,
+			default: false
+		},
+		width: {
+			type: String,
+			default: '84%'
+		},
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		},
+		padding: {
+			type: String,
+			default: '40rpx 64rpx'
+		},
+		radius: {
+			type: String,
+			default: '24rpx'
+		},
+		//标题
+		title: {
+			type: String,
+			default: ''
+		},
+		//内容
+		content: {
+			type: String,
+			default: ''
+		},
+		//内容字体颜色
+		color: {
+			type: String,
+			default: '#999'
+		},
+		//内容字体大小 rpx
+		size: {
+			type: Number,
+			default: 28
+		},
+		//形状 circle, square
+		shape: {
+			type: String,
+			default: 'square'
+		},
+		button: {
+			type: Array,
+			default: function() {
+				return [
+					{
+						text: '取消',
+						type: 'red',
+						plain: true //是否空心
+					},
+					{
+						text: '确定',
+						type: 'red',
+						plain: false
+					}
+				];
+			}
+		},
+		//点击遮罩 是否可关闭
+		maskClosable: {
+			type: Boolean,
+			default: true
+		},
+		//淡入效果,自定义弹框插入input输入框时传true
+		fadeIn: {
+			type: Boolean,
+			default: false
+		},
+		//自定义弹窗内容
+		custom: {
+			type: Boolean,
+			default: false
+		},
+		//容器z-index
+		zIndex:{
+			type: Number,
+			default: 9997
+		},
+		//mask z-index
+		maskZIndex:{
+			type: Number,
+			default: 9990
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		handleClick(e) {
+			if (!this.show) return;
+			const dataset = e.currentTarget.dataset;
+			this.$emit('click', {
+				index: Number(dataset.index)
+			});
+		},
+		handleClickCancel() {
+			if (!this.maskClosable) return;
+			this.$emit('cancel');
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-modal__container {
+	width: 100%;
+	height: 100%;
+	position: fixed;
+	left: 0;
+	top: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	visibility: hidden;
+}
+.tui-modal-box {
+	position: relative;
+	opacity: 0;
+	visibility: hidden;
+	box-sizing: border-box;
+	transition: all 0.3s ease-in-out;
+}
+
+.tui-modal-scale {
+	transform: scale(0);
+}
+
+.tui-modal-normal {
+	transform: scale(1);
+}
+
+.tui-modal-show {
+	opacity: 1;
+	visibility: visible;
+}
+
+.tui-modal-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	visibility: hidden;
+}
+
+.tui-mask-show {
+	visibility: visible;
+	opacity: 1;
+}
+
+.tui-modal-title {
+	text-align: center;
+	font-size: 34rpx;
+	color: #333;
+	padding-top: 20rpx;
+	font-weight: bold;
+}
+
+.tui-modal-content {
+	text-align: center;
+	color: #999;
+	font-size: 28rpx;
+	padding-top: 20rpx;
+	padding-bottom: 60rpx;
+}
+
+.tui-mtop {
+	margin-top: 30rpx;
+}
+
+.tui-mbtm {
+	margin-bottom: 30rpx;
+}
+
+.tui-modalBtn-box {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.tui-flex-column {
+	flex-direction: column;
+}
+
+.tui-modal-btn {
+	width: 46%;
+	height: 68rpx;
+	line-height: 68rpx;
+	position: relative;
+	border-radius: 10rpx;
+	font-size: 26rpx;
+	overflow: visible;
+	margin-left: 0;
+	margin-right: 0;
+}
+
+.tui-modal-btn::after {
+	content: ' ';
+	position: absolute;
+	width: 200%;
+	height: 200%;
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	transform: scale(0.5, 0.5) translateZ(0);
+	left: 0;
+	top: 0;
+	border-radius: 20rpx;
+	z-index: 2;
+}
+
+.tui-btn-width {
+	width: 80% !important;
+}
+
+.tui-primary {
+	background: #5677fc;
+	color: #fff;
+}
+
+.tui-primary-hover {
+	background: #4a67d6;
+	color: #e5e5e5;
+}
+
+.tui-primary-outline {
+	color: #5677fc;
+	background: transparent;
+}
+
+.tui-primary-outline::after {
+	border: 1px solid #5677fc;
+}
+
+.tui-danger {
+	background: #ed3f14;
+	color: #fff;
+}
+
+.tui-danger-hover {
+	background: #d53912;
+	color: #e5e5e5;
+}
+
+.tui-danger-outline {
+	color: #ed3f14;
+	background: transparent;
+}
+
+.tui-danger-outline::after {
+	border: 1px solid #ed3f14;
+}
+
+.tui-red {
+	background: #e41f19;
+	color: #fff;
+}
+
+.tui-red-hover {
+	background: #c51a15;
+	color: #e5e5e5;
+}
+
+.tui-red-outline {
+	color: #e41f19;
+	background: transparent;
+}
+
+.tui-red-outline::after {
+	border: 1px solid #e41f19;
+}
+
+.tui-warning {
+	background: #ff7900;
+	color: #fff;
+}
+
+.tui-warning-hover {
+	background: #e56d00;
+	color: #e5e5e5;
+}
+
+.tui-warning-outline {
+	color: #ff7900;
+	background: transparent;
+}
+
+.tui-warning-outline::after {
+	border: 1px solid #ff7900;
+}
+
+.tui-green {
+	background: #19be6b;
+	color: #fff;
+}
+
+.tui-green-hover {
+	background: #16ab60;
+	color: #e5e5e5;
+}
+
+.tui-green-outline {
+	color: #19be6b;
+	background: transparent;
+}
+
+.tui-green-outline::after {
+	border: 1px solid #19be6b;
+}
+
+.tui-white {
+	background: #fff;
+	color: #333;
+}
+
+.tui-white-hover {
+	background: #f7f7f9;
+	color: #666;
+}
+
+.tui-white-outline {
+	color: #333;
+	background: transparent;
+}
+
+.tui-white-outline::after {
+	border: 1px solid #333;
+}
+
+.tui-gray {
+	background: #ededed;
+	color: #999;
+}
+
+.tui-gray-hover {
+	background: #d5d5d5;
+	color: #898989;
+}
+
+.tui-gray-outline {
+	color: #999;
+	background: transparent;
+}
+
+.tui-gray-outline::after {
+	border: 1px solid #999;
+}
+
+.tui-outline-hover {
+	opacity: 0.6;
+}
+
+.tui-circle-btn {
+	border-radius: 40rpx !important;
+}
+
+.tui-circle-btn::after {
+	border-radius: 80rpx !important;
+}
+</style>

+ 250 - 0
components/thorui/tui-navigation-bar/tui-navigation-bar.vue

@@ -0,0 +1,250 @@
+<template>
+	<view
+		class="tui-navigation-bar"
+		:class="{ 'tui-bar-line': opacity > 0.85 && splitLine, 'tui-navbar-fixed': isFixed, 'tui-backdrop__filter': backdropFilter && dropDownOpacity > 0  }"
+		:style="{ height: height + 'px', backgroundColor: `rgba(${background},${opacity})`, opacity: dropDownOpacity, zIndex: isFixed ? zIndex : 'auto' }"
+	>
+		<view class="tui-status-bar" :style="{ height: statusBarHeight + 'px' }" v-if="isImmersive"></view>
+		<view
+			class="tui-navigation_bar-title"
+			:style="{ opacity: transparent || opacity >= maxOpacity ? 1 : opacity, color: color, paddingTop: top - statusBarHeight + 'px' }"
+			v-if="title && !isCustom"
+		>
+			{{ title }}
+		</view>
+		<slot />
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiNavigationBar',
+	props: {
+		//NavigationBar标题
+		title: {
+			type: String,
+			default: ''
+		},
+		//NavigationBar标题颜色
+		color: {
+			type: String,
+			default: '#333'
+		},
+		//NavigationBar背景颜色,不支持rgb
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		},
+		//是否需要分割线
+		splitLine: {
+			type: Boolean,
+			default: false
+		},
+		//是否设置不透明度
+		isOpacity: {
+			type: Boolean,
+			default: true
+		},
+		//不透明度最大值 0-1
+		maxOpacity: {
+			type: [Number, String],
+			default: 1
+		},
+		//背景透明 【设置该属性,则背景透明,只出现内容,isOpacity和maxOpacity失效】
+		transparent: {
+			type: Boolean,
+			default: false
+		},
+		//滚动条滚动距离
+		scrollTop: {
+			type: [Number, String],
+			default: 0
+		},
+		/*
+			 isOpacity 为true时生效
+			 opacity=scrollTop /windowWidth * scrollRatio
+			*/
+		scrollRatio: {
+			type: [Number, String],
+			default: 0.3
+		},
+		//是否自定义header内容
+		isCustom: {
+			type: Boolean,
+			default: false
+		},
+		//是否沉浸式
+		isImmersive: {
+			type: Boolean,
+			default: true
+		},
+		isFixed: {
+			type: Boolean,
+			default: true
+		},
+		//是否开启高斯模糊效果[仅在支持的浏览器有效果]
+		backdropFilter: {
+			type: Boolean,
+			default: false
+		},
+		//下拉隐藏NavigationBar,主要针对有回弹效果ios端
+		dropDownHide: {
+			type: Boolean,
+			default: false
+		},
+		//z-index设置
+		zIndex: {
+			type: [Number, String],
+			default: 9998
+		}
+	},
+	watch: {
+		scrollTop(newValue, oldValue) {
+			if (this.isOpacity && !this.transparent) {
+				this.opacityChange();
+			}
+		},
+		backgroundColor(val) {
+			if (val) {
+				this.background = this.hexToRgb(val);
+			}
+		}
+	},
+	data() {
+		return {
+			width: 375, //header宽度
+			left: 375, //小程序端 左侧距胶囊按钮距离
+			height: 44, //header高度
+			top: 0,
+			scrollH: 1, //滚动总高度,计算opacity
+			opacity: 1, //0-1
+			statusBarHeight: 0, //状态栏高度
+			background: '255,255,255', //header背景色
+			dropDownOpacity: 1
+		};
+	},
+	created() {
+		this.dropDownOpacity = this.backdropFilter && 0;
+		this.opacity = this.isOpacity || this.transparent ? 0 : this.maxOpacity;
+		this.background = this.hexToRgb(this.backgroundColor);
+		let obj = {};
+		// #ifdef MP-WEIXIN
+		obj = wx.getMenuButtonBoundingClientRect();
+		// #endif
+		// #ifdef MP-BAIDU
+		obj = swan.getMenuButtonBoundingClientRect();
+		// #endif
+		// #ifdef MP-ALIPAY
+		my.hideAddToDesktopMenu();
+		// #endif
+		uni.getSystemInfo({
+			success: res => {
+				this.statusBarHeight = res.statusBarHeight;
+				this.width = res.windowWidth;
+				this.left = obj.left || res.windowWidth;
+				if (this.isImmersive) {
+					this.height = obj.top ? obj.top + obj.height + 8 : res.statusBarHeight + 44;
+				}
+				this.scrollH = res.windowWidth * this.scrollRatio;
+				this.top = obj.top ? obj.top + (obj.height - 32) / 2 : res.statusBarHeight + 6;
+				this.$emit('init', {
+					width: this.width,
+					height: this.height,
+					left: this.left,
+					top: this.top,
+					statusBarHeight: this.statusBarHeight,
+					opacity: this.opacity,
+					windowHeight: res.windowHeight
+				});
+			}
+		});
+	},
+	methods: {
+		hexToRgb(hex) {
+			let rgb = '255,255,255';
+			if (hex && ~hex.indexOf('#')) {
+				if (hex.length === 4) {
+					let text = hex.substring(1, 4);
+					hex = '#' + text + text;
+				}
+				let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+				if (result) {
+					rgb = `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`;
+				}
+			}
+			return rgb;
+		},
+		opacityChange() {
+			if (this.dropDownHide) {
+				if (this.scrollTop < 0) {
+					if (this.dropDownOpacity > 0) {
+						this.dropDownOpacity = 1 - Math.abs(this.scrollTop) / 30;
+					}
+				} else {
+					this.dropDownOpacity = 1;
+				}
+			}
+
+			let scroll = this.scrollTop <= 1 ? 0 : this.scrollTop;
+			let opacity = scroll / this.scrollH;
+			if ((this.opacity >= this.maxOpacity && opacity >= this.maxOpacity) || (this.opacity == 0 && opacity == 0)) {
+				return;
+			}
+			this.opacity = opacity > this.maxOpacity ? this.maxOpacity : opacity;
+			if (this.backdropFilter) {
+				this.dropDownOpacity = this.opacity >= this.maxOpacity ? 1 : this.opacity;
+			}
+			this.$emit('change', {
+				opacity: this.opacity
+			});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-navigation-bar {
+	width: 100%;
+	transition: opacity 0.4s;
+}
+.tui-backdrop__filter {
+	/* Safari for macOS & iOS */
+	-webkit-backdrop-filter: blur(15px);
+	/* Google Chrome */
+	backdrop-filter: blur(15px);
+}
+
+.tui-navbar-fixed {
+	position: fixed;
+	left: 0;
+	top: 0;
+}
+
+.tui-status-bar {
+	width: 100%;
+}
+
+.tui-navigation_bar-title {
+	width: 100%;
+	font-size: 17px;
+	line-height: 17px;
+	/* #ifndef APP-PLUS */
+	font-weight: 500;
+	/* #endif */
+	height: 32px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-bar-line::after {
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+	bottom: 0;
+	right: 0;
+	left: 0;
+}
+</style>

+ 117 - 0
components/thorui/tui-no-data/tui-no-data.vue

@@ -0,0 +1,117 @@
+<template>
+	<view class="tui-nodata-box" :class="[fixed?'tui-nodata-fixed':'']">
+		<image v-if="imgUrl" :src="imgUrl" class="tui-tips-icon" :style="{width:imgWidth+'rpx',height:imgHeight+'rpx'}"></image>
+		<view class="tui-tips-content">
+			<slot></slot>
+		</view>
+		<view class="tui-tips-btn" hover-class="tui-btn__hover" :hover-stay-time="150" :style="{width:btnWidth+'rpx',height:btnHeight+'rpx',background:backgroundColor,borderRadius:radius,fontSize:size+'rpx'}" v-if="btnText"  @tap="handleClick">{{btnText}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiNoData",
+		props: {
+			//是否垂直居中
+			fixed: {
+				type: Boolean,
+				default: true
+			},
+			//图片地址,没有则不显示
+			imgUrl: {
+				type: String,
+				default: ""
+			},
+			//图片宽度
+			imgWidth: {
+				type: Number,
+				default: 200
+			},
+			//图片高度
+			imgHeight:{
+				type: Number,
+				default: 200
+			},
+			//按钮宽度
+			btnWidth:{
+				type: Number,
+				default: 200
+			},
+			btnHeight:{
+				type: Number,
+				default: 60
+			},
+			//按钮文字,没有则不显示
+			btnText:{
+				type:String,
+				default: ""
+			},
+			//按钮背景色
+			backgroundColor:{
+				type:String,
+				default: "#EB0909"
+			},
+			size:{
+				type:Number,
+				default:28
+			},
+			radius:{
+				type:String,
+				default:'8rpx'
+			}
+		},
+		methods: {
+			handleClick(e) {
+				this.$emit('click', {});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-nodata-box {
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.tui-nodata-fixed {
+		width: 90%;
+		position: fixed;
+		left: 50%;
+		top: 50%;
+		-webkit-transform: translate(-50%, -50%);
+		transform: translate(-50%, -50%);
+	}
+
+	.tui-tips-icon {
+		display: block;
+		flex-shrink: 0;
+		width: 280rpx;
+		height: 280rpx;
+		margin-bottom: 40rpx;
+	}
+
+	.tui-tips-content {
+		text-align: center;
+		color: #666666;
+		font-size: 28rpx;
+		padding: 0 50rpx 28rpx 50rpx;
+		box-sizing: border-box;
+		word-break: break-all;
+		word-wrap: break-word;
+	}
+
+	.tui-tips-btn {
+		color: #fff;
+		margin: 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+	.tui-btn__hover{
+		opacity: 0.5;
+	}
+	
+</style>

+ 115 - 0
components/thorui/tui-nomore/tui-nomore.vue

@@ -0,0 +1,115 @@
+<template>
+	<view class="tui-nomore-class tui-loadmore-none">
+		<view :class="[isDot?'tui-nomore-dot':'tui-nomore']">
+			<view :style="{backgroundColor:backgroundColor}" :class="[isDot?'tui-dot-text':'tui-nomore-text']">{{isDot?dotText:text}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiNomore",
+		props: {
+			//当前页面背景颜色
+			backgroundColor: {
+				type: String,
+				default: "#fafafa"
+			},
+			//是否以圆点代替 "没有更多了"
+			isDot: {
+				type: Boolean,
+				default: false
+			},
+			//isDot为false时生效
+			text: {
+				type: String,
+				default: "没有更多了"
+			}
+		},
+		data() {
+			return {
+				dotText: "●"
+			};
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-loadmore-none {
+		width: 50%;
+		margin: 1.5em auto;
+		line-height: 1.5em;
+		font-size: 24rpx;
+		display: flex;
+		justify-content: center;
+	}
+
+	.tui-nomore {
+		width: 100%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: center;
+		margin-top: 10rpx;
+		padding-bottom: 6rpx;
+	}
+
+	.tui-nomore::before {
+		content: ' ';
+		position: absolute;
+		border-bottom: 1rpx solid #e5e5e5;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		width: 100%;
+		top: 18rpx;
+		left: 0;
+	}
+
+	.tui-nomore-text {
+		color: #999;
+		font-size: 24rpx;
+		text-align: center;
+		padding: 0 18rpx;
+		height: 36rpx;
+		line-height: 36rpx;
+		position: relative;
+		z-index: 1;
+	}
+
+	.tui-nomore-dot {
+		position: relative;
+		text-align: center;
+		-webkit-display: flex;
+		display: flex;
+		-webkit-justify-content: center;
+		justify-content: center;
+		margin-top: 10rpx;
+		padding-bottom: 6rpx;
+	}
+
+	.tui-nomore-dot::before {
+		content: '';
+		position: absolute;
+		border-bottom: 1rpx solid #e5e5e5;
+		-webkit-transform: scaleY(0.5)  translateX(-50%);
+		transform: scaleY(0.5)  translateX(-50%);
+		width: 360rpx;
+		top: 18rpx;
+		left: 50%;
+	}
+
+	.tui-dot-text {
+		position: relative;
+		color: #e5e5e5;
+		font-size: 10px;
+		text-align: center;
+		width: 50rpx;
+		height: 36rpx;
+		line-height: 36rpx;
+		-webkit-transform: scale(0.8);
+		transform: scale(0.8);
+		-webkit-transform-origin: center center;
+		transform-origin: center center;
+		z-index: 1;
+	}
+</style>

+ 205 - 0
components/thorui/tui-numberbox/tui-numberbox.vue

@@ -0,0 +1,205 @@
+<template>
+	<view class="tui-numberbox">
+		<view class="tui-numbox-icon tui-icon-reduce " :class="[disabled || min>=value?'tui-disabled':'']" @tap.stop="reduce"
+		 :style="{color:iconColor,fontSize:iconSize+'rpx'}"></view>
+		<input type="number" v-model="inputValue" :disabled="disabled" @blur="blur" class="tui-num-input" :style="{color:color,fontSize:size+'rpx',backgroundColor:backgroundColor,height:height+'rpx',minHeight:height+'rpx',width:width+'rpx'}" />
+		<view class="tui-numbox-icon tui-icon-plus" :class="[disabled || value>=max?'tui-disabled':'']" @tap.stop="plus" :style="{color:iconColor,fontSize:iconSize+'rpx'}"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiNumberbox",
+		props: {
+			value: {
+				type: Number,
+				default: 1
+			},
+			//最小值
+			min: {
+				type: Number,
+				default: 1
+			},
+			//最大值
+			max: {
+				type: Number,
+				default: 99
+			},
+			//迈步大小 1 1.1 10...
+			step: {
+				type: Number,
+				default: 1
+			},
+			//是否禁用操作
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//加减图标大小 rpx
+			iconSize: {
+				type: Number,
+				default: 26
+			},
+			iconColor: {
+				type: String,
+				default: "#666666"
+			},
+			//input 高度
+			height: {
+				type: Number,
+				default: 42
+			},
+			//input 宽度
+			width: {
+				type: Number,
+				default: 80
+			},
+			size: {
+				type: Number,
+				default: 28
+			},
+			//input 背景颜色
+			backgroundColor: {
+				type: String,
+				default: "#F5F5F5"
+			},
+			//input 字体颜色
+			color: {
+				type: String,
+				default: "#333"
+			},
+			//索引值,列表中使用
+			index: {
+				type: [Number, String],
+				default: 0
+			},
+			//自定义参数
+			custom: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		created() {
+			this.inputValue = +this.value
+		},
+		data() {
+			return {
+				inputValue: 0
+			};
+		},
+		watch: {
+			value(val) {
+				this.inputValue = +val
+			}
+		},
+		methods: {
+			getScale() {
+				let scale = 1;
+				//浮点型
+				if (!Number.isInteger(this.step)) {
+					scale = Math.pow(10, (this.step + '').split('.')[1].length)
+				}
+				return scale;
+			},
+			calcNum: function(type) {
+				if (this.disabled) {
+					return
+				}
+				const scale = this.getScale()
+				let num = this.value * scale
+				let step = this.step * scale
+				if (type === 'reduce') {
+					num -= step
+				} else if (type === 'plus') {
+					num += step
+				}
+				let value = num / scale
+				if (type === "plus" && value < this.min) {
+					value = this.min
+				} else if (type === "reduce" && value > this.max) {
+					value = this.max
+				}
+				if (value < this.min || value > this.max) {
+					return
+				}
+				this.handleChange(value, type)
+			},
+			plus: function() {
+				this.calcNum("plus")
+			},
+			reduce: function() {
+				this.calcNum("reduce")
+			},
+			blur: function(e) {
+				let value = e.detail.value
+				if (value) {
+					if (~value.indexOf(".") && Number.isInteger(this.step)) {
+						value = value.split(".")[0]
+					}
+					value = Number(value)
+					if (value > this.max) {
+						value = this.max
+					} else if (value < this.min) {
+						value = this.min
+					}
+				} else {
+					value = this.min
+				}
+				if ((value == this.value && value != this.inputValue) || !e.detail.value) {
+					this.inputValue = value
+				}
+				this.handleChange(value, "blur")
+			},
+			handleChange(value, type) {
+				if (this.disabled) return;
+				this.$emit('change', {
+					value: value,
+					type: type,
+					index: this.index,
+					custom: this.custom
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'numberbox';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAASQAA0AAAAABtwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEdAAAABoAAAAciBpnRUdERUYAAARUAAAAHgAAAB4AKQALT1MvMgAAAZwAAABDAAAAVjxzSINjbWFwAAAB9AAAAEYAAAFK5zLpOGdhc3AAAARMAAAACAAAAAj//wADZ2x5ZgAAAkgAAACHAAAAnIfIEjxoZWFkAAABMAAAAC8AAAA2FZWEOWhoZWEAAAFgAAAAHAAAACQH3gOFaG10eAAAAeAAAAARAAAAEgwAAAFsb2NhAAACPAAAAAwAAAAMADAATm1heHAAAAF8AAAAHwAAACABEAAobmFtZQAAAtAAAAFJAAACiCnmEVVwb3N0AAAEHAAAAC0AAABV/+8iFXjaY2BkYGAA4gVmC5Tj+W2+MnCzMIDATWsFOQT9v5GFgbkeyOVgYAKJAgDrogf+AHjaY2BkYGBu+N/AEMPCAAJAkpEBFbAAAEcKAm142mNgZGBgYGWQYQDRDAxMQMwFhAwM/8F8BgALpAE5AHjaY2BkYWCcwMDKwMDUyXSGgYGhH0IzvmYwYuQAijKwMjNgBQFprikMDs9Yn01kbvjfwBDD3MDQABRmBMkBAOXpDHEAeNpjYYAAFghmZGAAAACdAA4AAAB42mNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZiesT6b+P8/AwOElvwnWQxVDwSMbAxwDiMTkGBiQAWMDMMeAABRZwszAAAAAAAAAAAAAAAwAE542iWKQQrCMBBF5xNpd0pQ7EIoTEnahSCTUNqdWz2A9TrieXKeXCc1qcPn/zfzh0BYv2pVH7oQgbvqdG5Yt/DTrNlPYz+wHvuuqhFSME4sFshTgKUsKfhH5lg8BSul3i5bS3mQdd0RIh2IjnvUrkXDd8zuhuFt86tY9fonIsSYgsXpB+cCGosAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMGiTIxMjMwiWZmJQJRXVQoigTgjMd9QGIsgAFDsEBsAAAAAAAAB//8AAgABAAAADAAAABYAAAACAAEAAwAEAAEABAAAAAIAAAAAeNpjYGBgZACCq0vUOUD0TWsFORgNADPBBE4AAA==) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-numbox-icon {
+		font-family: "numberbox" !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+		padding: 10rpx;
+	}
+
+	.tui-icon-reduce:before {
+		content: "\e691";
+	}
+
+	.tui-icon-plus:before {
+		content: "\e605";
+	}
+
+	.tui-numberbox {
+		display: -webkit-inline-flex;
+		display: inline-flex;
+		align-items: center;
+	}
+
+	.tui-num-input {
+		text-align: center;
+		margin: 0 12rpx;
+		font-weight: 400;
+	}
+
+	.tui-disabled {
+		color: #ededed !important;
+	}
+</style>

+ 699 - 0
components/thorui/tui-picture-cropper/tui-picture-cropper.vue

@@ -0,0 +1,699 @@
+<template>
+	<view class="tui-container" @touchmove.stop.prevent="stop">
+		<view
+			class="tui-image-cropper"
+			:change:prop="parse.propsChange"
+			:prop="props"
+			:data-lockRatio="lockRatio"
+			:data-lockWidth="lockWidth"
+			:data-lockHeight="lockHeight"
+			:data-maxWidth="maxWidth"
+			:data-minWidth="minWidth"
+			:data-maxHeight="maxHeight"
+			:data-minHeight="minHeight"
+			:data-width="width"
+			:data-height="height"
+			:data-limitMove="limitMove"
+			:data-windowHeight="sysInfo.windowHeight || 600"
+			:data-windowWidth="sysInfo.windowWidth || 400"
+			:data-imgTop="imgTop"
+			:data-imgLeft="imgLeft"
+			:data-imgWidth="imgWidth"
+			:data-imgHeight="imgHeight"
+			:data-angle="angle"
+			@touchend="parse.cutTouchEnd"
+			@touchstart="parse.cutTouchStart"
+			@touchmove="parse.cutTouchMove"
+		>
+			<view class="tui-content">
+				<view class="tui-content-top tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				<view class="tui-content-middle">
+					<view class="tui-bg-transparent tui-wxs-bg" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+					<view class="tui-cropper-box" :style="{ borderColor: borderColor, transitionProperty: cutAnimation ? '' : 'background' }">
+						<view
+							v-for="(item, index) in 4"
+							:key="index"
+							class="tui-edge"
+							:class="[`tui-${index < 2 ? 'top' : 'bottom'}-${index === 0 || index === 2 ? 'left' : 'right'}`]"
+							:style="{
+								width: edgeWidth,
+								height: edgeWidth,
+								borderColor: edgeColor,
+								borderWidth: edgeBorderWidth,
+								left: index === 0 || index === 2 ? `-${edgeOffsets}` : 'auto',
+								right: index === 1 || index === 3 ? `-${edgeOffsets}` : 'auto',
+								top: index < 2 ? `-${edgeOffsets}` : 'auto',
+								bottom: index > 1 ? `-${edgeOffsets}` : 'auto'
+							}"
+						></view>
+					</view>
+					<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				</view>
+				<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+			</view>
+			<image
+				@load="imageLoad"
+				@error="imageLoad"
+				@touchstart="parse.touchstart"
+				@touchmove="parse.touchmove"
+				@touchend="parse.touchend"
+				:data-minScale="minScale"
+				:data-maxScale="maxScale"
+				:data-disableRotate="disableRotate"
+				:style="{
+					width: imgWidth ? imgWidth + 'px' : 'auto',
+					height: imgHeight ? imgHeight + 'px' : 'auto',
+					transitionDuration: (cutAnimation ? 0.3 : 0) + 's'
+				}"
+				class="tui-cropper-image"
+				:src="imageUrl"
+				v-if="imageUrl"
+				mode="widthFix"
+			></image>
+		</view>
+		<canvas
+			canvas-id="tui-image-cropper"
+			id="tui-image-cropper"
+			:disable-scroll="true"
+			:style="{ width: CROPPER_WIDTH * scaleRatio + 'px', height: CROPPER_HEIGHT * scaleRatio + 'px' }"
+			class="tui-cropper-canvas"
+		></canvas>
+		<view class="tui-cropper-tabbar" v-if="!custom">
+			<view class="tui-op-btn" @tap.stop="back">取消</view>
+			<image :src="rotateImg" class="tui-rotate-img" @tap="setAngle"></image>
+			<view class="tui-op-btn" @tap.stop="getImage">完成</view>
+		</view>
+	</view>
+</template>
+<script src="./tui-picture-cropper.wxs" module="parse" lang="wxs"></script>
+<script>
+/**
+ * 注意:组件中使用的图片地址,将文件复制到自己项目中
+ * 如果图片位置与组件同级,编译成小程序时图片会丢失
+ * 拷贝static下整个components文件夹
+ *也可直接转成base64(不建议)
+ * */
+export default {
+	name: 'tuiPictureCropper',
+	props: {
+		//图片路径
+		imageUrl: {
+			type: String,
+			default: ''
+		},
+		/*
+					 默认正方形,可修改大小控制比例
+					 裁剪框高度 px
+					*/
+		height: {
+			type: Number,
+			default: 280
+		},
+		//裁剪框宽度 px
+		width: {
+			type: Number,
+			default: 280
+		},
+		//裁剪框最小宽度 px
+		minWidth: {
+			type: Number,
+			default: 100
+		},
+		//裁剪框最小高度 px
+		minHeight: {
+			type: Number,
+			default: 100
+		},
+		//裁剪框最大宽度 px
+		maxWidth: {
+			type: Number,
+			default: 360
+		},
+		//裁剪框最大高度 px
+		maxHeight: {
+			type: Number,
+			default: 360
+		},
+		//裁剪框border颜色
+		borderColor: {
+			type: String,
+			default: 'rgba(255,255,255,0.1)'
+		},
+		//裁剪框边缘线颜色
+		edgeColor: {
+			type: String,
+			default: '#FFFFFF'
+		},
+		//裁剪框边缘线宽度 w=h
+		edgeWidth: {
+			type: String,
+			default: '34rpx'
+		},
+		//裁剪框边缘线border宽度
+		edgeBorderWidth: {
+			type: String,
+			default: '6rpx'
+		},
+		//偏移距离,根据edgeBorderWidth进行调整
+		edgeOffsets: {
+			type: String,
+			default: '6rpx'
+		},
+		/**
+		 * 如果宽度和高度都为true则裁剪框禁止拖动
+		 * 裁剪框宽度锁定
+		 */
+		lockWidth: {
+			type: Boolean,
+			default: false
+		},
+		//裁剪框高度锁定
+		lockHeight: {
+			type: Boolean,
+			default: false
+		},
+		//锁定裁剪框比例(放大或缩小)
+		lockRatio: {
+			type: Boolean,
+			default: false
+		},
+		//生成的图片尺寸相对剪裁框的比例
+		scaleRatio: {
+			type: Number,
+			default: 2
+		},
+		//图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
+		quality: {
+			type: Number,
+			default: 0.8
+		},
+		//图片旋转角度
+		rotateAngle: {
+			type: Number,
+			default: 0
+		},
+		//图片最小缩放比
+		minScale: {
+			type: Number,
+			default: 0.5
+		},
+		//图片最大缩放比
+		maxScale: {
+			type: Number,
+			default: 2
+		},
+		//是否禁用触摸旋转(为false则可以触摸转动图片,limitMove为false生效)
+		disableRotate: {
+			type: Boolean,
+			default: true
+		},
+		//是否限制移动范围(剪裁框只能在图片内,为true不可触摸转动图片)
+		limitMove: {
+			type: Boolean,
+			default: true
+		},
+		//自定义操作栏(为true时隐藏底部操作栏)
+		custom: {
+			type: Boolean,
+			default: false
+		},
+		//值发生改变开始裁剪(custom为true时生效)
+		startCutting: {
+			type: [Number, Boolean],
+			default: 0
+		},
+		/**
+		 * 是否返回base64(H5端默认base64)
+		 * 支持平台:App,微信小程序,支付宝小程序,H5(默认url就是base64)
+		 **/
+		isBase64: {
+			type: Boolean,
+			default: false
+		},
+		//裁剪时是否显示loadding
+		loadding: {
+			type: Boolean,
+			default: true
+		},
+		//旋转icon
+		rotateImg: {
+			type: String,
+			default: '/static/components/cropper/img_rotate.png'
+		}
+	},
+	data() {
+		return {
+			TIME_CUT_CENTER: null,
+			CROPPER_WIDTH: 200, //裁剪框宽
+			CROPPER_HEIGHT: 200, //裁剪框高
+			cutX: 0, //画布x轴起点
+			cutY: 0, //画布y轴起点0
+			canvasWidth: 0,
+			canvasHeight: 0,
+			imgWidth: 0, //图片宽度
+			imgHeight: 0, //图片高度
+			scale: 1, //图片缩放比
+			angle: 0, //图片旋转角度
+			cutAnimation: false, //是否开启图片和裁剪框过渡
+			cutAnimationTime: null,
+			imgTop: 0, //图片上边距
+			imgLeft: 0, //图片左边距
+			ctx: null,
+			sysInfo: {},
+			props: '',
+			sizeChange: 0, //2
+			angleChange: 0, //3
+			resetChange: 0, //4
+			centerChange: 0 //5
+		};
+	},
+	watch: {
+		//定义变量然后利用change触发
+		imageUrl(val, oldVal) {
+			this.imageReset();
+			this.showLoading();
+			uni.getImageInfo({
+				src: val,
+				success: res => {
+					//计算图片尺寸
+					this.imgComputeSize(res.width, res.height);
+					if (this.limitMove) {
+						this.angleChange++;
+						this.props = `3,${this.angleChange}`;
+					}
+				},
+				fail: err => {
+					this.imgComputeSize();
+					if (this.limitMove) {
+						this.angleChange++;
+						this.props = `3,${this.angleChange}`;
+					}
+				}
+			});
+		},
+		rotateAngle(val) {
+			this.cutAnimation = true;
+			this.angle = val;
+			this.angleChanged(val);
+		},
+		cutAnimation(val) {
+			//开启过渡260毫秒之后自动关闭
+			clearTimeout(this.cutAnimationTime);
+			if (val) {
+				this.cutAnimationTime = setTimeout(() => {
+					this.cutAnimation = false;
+				}, 260);
+			}
+		},
+		limitMove(val) {
+			if (val) {
+				this.angleChanged(this.angle);
+			}
+		},
+		startCutting(val) {
+			if (this.custom && val) {
+				this.getImage();
+			}
+		}
+	},
+	mounted() {
+		this.sysInfo = uni.getSystemInfoSync();
+		this.imgTop = this.sysInfo.windowHeight / 2;
+		this.imgLeft = this.sysInfo.windowWidth / 2;
+		this.CROPPER_WIDTH = this.width;
+		this.CROPPER_HEIGHT = this.height;
+		this.canvasHeight = this.height;
+		this.canvasWidth = this.width;
+		this.ctx = uni.createCanvasContext('tui-image-cropper', this);
+		//初始化
+		setTimeout(() => {
+			this.props = '1,1';
+		}, 0);
+		setTimeout(() => {
+			this.$emit('ready', {});
+		}, 200);
+	},
+	methods: {
+		//网络图片转成本地文件[同步执行]
+		async getLocalImage(url) {
+			return await new Promise((resolve, reject) => {
+				uni.downloadFile({
+					url: url,
+					success: res => {
+						resolve(res.tempFilePath);
+					},
+					fail: res => {
+						reject(false)
+					}
+				})
+			})
+		},
+		//返回裁剪后图片信息
+		getImage() {
+			if (!this.imageUrl) {
+				uni.showToast({
+					title: '请选择图片',
+					icon: 'none'
+				});
+				return;
+			}
+			this.loadding && this.showLoading();
+			let draw =async () => {
+				//图片实际大小
+				let imgWidth = this.imgWidth * this.scale * this.scaleRatio;
+				let imgHeight = this.imgHeight * this.scale * this.scaleRatio;
+				//canvas和图片的相对距离
+				let xpos = this.imgLeft - this.cutX;
+				let ypos = this.imgTop - this.cutY;
+				//旋转画布
+				this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio);
+				this.ctx.rotate((this.angle * Math.PI) / 180);
+				let imgUrl = this.imageUrl;
+				// #ifdef APP-PLUS || MP-WEIXIN
+				if (~this.imageUrl.indexOf('https:')) {
+					imgUrl = await this.getLocalImage(this.imageUrl)
+				}
+				// #endif
+				this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
+				this.ctx.draw(false, () => {
+					let params = {
+						width: this.canvasWidth * this.scaleRatio,
+						height: Math.round(this.canvasHeight * this.scaleRatio),
+						destWidth: this.canvasWidth * this.scaleRatio,
+						destHeight: Math.round(this.canvasHeight) * this.scaleRatio,
+						fileType: 'png',
+						quality: this.quality
+					};
+					let data = {
+						url: '',
+						base64: '',
+						width: this.canvasWidth * this.scaleRatio,
+						height: this.canvasHeight * this.scaleRatio
+					};
+					// #ifdef MP-ALIPAY
+
+					if (this.isBase64) {
+						this.ctx.toDataURL(params).then(dataURL => {
+							data.base64 = dataURL;
+							this.loadding && uni.hideLoading();
+							this.$emit('cropper', data);
+						});
+					} else {
+						this.ctx.toTempFilePath({
+							...params,
+							success: res => {
+								data.url = res.tempFilePath;
+								this.loadding && uni.hideLoading();
+								this.$emit('cropper', data);
+							}
+						});
+					}
+					// #endif
+
+					// #ifndef MP-ALIPAY
+					// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+					this.isBase64 = false;
+					// #endif
+					if (this.isBase64) {
+						uni.canvasGetImageData({
+							canvasId: 'tui-image-cropper',
+							x: 0,
+							y: 0,
+							width: this.canvasWidth * this.scaleRatio,
+							height: Math.round(this.canvasHeight * this.scaleRatio),
+							success: res => {
+								const arrayBuffer = new Uint8Array(res.data);
+								const base64 = uni.arrayBufferToBase64(arrayBuffer);
+								data.base64 = base64;
+								this.loadding && uni.hideLoading();
+								this.$emit('cropper', data);
+							}
+						},this);
+					} else {
+						uni.canvasToTempFilePath(
+							{
+								...params,
+								canvasId: 'tui-image-cropper',
+								success: res => {
+									data.url = res.tempFilePath;
+									// #ifdef H5
+									data.base64 = res.tempFilePath;
+									// #endif
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								},
+								fail(res) {
+									console.log(res);
+								}
+							},
+							this
+						);
+					}
+					// #endif
+				});
+			};
+			if (this.CROPPER_WIDTH != this.canvasWidth || this.CROPPER_HEIGHT != this.canvasHeight) {
+				this.CROPPER_WIDTH = this.canvasWidth;
+				this.CROPPER_HEIGHT = this.canvasHeight;
+				this.$nextTick(() => {
+					this.ctx.draw();
+					setTimeout(() => {
+						draw();
+					}, 100);
+				});
+			} else {
+				draw();
+			}
+		},
+		change(e) {
+			this.cutX = e.cutX || 0;
+			this.cutY = e.cutY || 0;
+			this.canvasWidth = e.canvasWidth || this.width;
+			this.canvasHeight = e.canvasHeight || this.height;
+			this.imgWidth = e.imgWidth || this.imgWidth;
+			this.imgHeight = e.imgHeight || this.imgHeight;
+			this.scale = e.scale || 1;
+			this.angle = e.angle || 0;
+			this.imgTop = e.imgTop || 0;
+			this.imgLeft = e.imgLeft || 0;
+		},
+		imageReset() {
+			this.scale = 1;
+			this.angle = 0;
+			let sys = this.sysInfo.windowHeight ? this.sysInfo : uni.getSystemInfoSync();
+			this.imgTop = sys.windowHeight / 2;
+			this.imgLeft = sys.windowWidth / 2;
+			this.resetChange++;
+			this.props = `4,${this.resetChange}`;
+			//初始化旋转角度 0deg
+			this.$emit('initAngle', {});
+		},
+		imageLoad(e) {
+			this.imageReset();
+			uni.hideLoading();
+			this.$emit('imageLoad', {});
+		},
+
+		imgComputeSize(width, height) {
+			//默认按图片最小边 = 对应裁剪框尺寸
+			let imgWidth = width,
+				imgHeight = height;
+			if (imgWidth && imgHeight) {
+				if (imgWidth / imgHeight > this.width / this.height) {
+					imgHeight = this.height;
+					imgWidth = (width / height) * imgHeight;
+				} else {
+					imgWidth = this.width;
+					imgHeight = (height / width) * imgWidth;
+				}
+			} else {
+				let sys = this.sysInfo || uni.getSystemInfoSync();
+				imgWidth = sys.windowWidth;
+				imgHeight = 0;
+			}
+			this.imgWidth = imgWidth;
+			this.imgHeight = imgHeight;
+			this.sizeChange++;
+			this.props = `2,${this.sizeChange}`;
+		},
+		moveStop() {
+			clearTimeout(this.TIME_CUT_CENTER);
+			this.TIME_CUT_CENTER = setTimeout(() => {
+				if (!this.cutAnimation) {
+					this.cutAnimation = true;
+				}
+				this.centerChange++;
+				this.props = `5,${this.centerChange}`;
+			}, 666);
+		},
+		moveDuring() {
+			clearTimeout(this.TIME_CUT_CENTER);
+		},
+		showLoading() {
+			uni.showLoading({
+				title: '请稍候...',
+				mask: true
+			});
+		},
+		stop() {},
+		back() {
+			uni.navigateBack();
+		},
+		angleChanged(val) {
+			this.moveStop();
+			if (this.limitMove && val % 90) {
+				this.angle = Math.round(val / 90) * 90;
+			}
+			this.angleChange++;
+			this.props = `3,${this.angleChange}`;
+		},
+		setAngle() {
+			this.cutAnimation = true;
+			this.angle = this.angle + 90;
+			this.angleChanged(this.angle);
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-container {
+	width: 100vw;
+	height: 100vh;
+	background-color: rgba(0, 0, 0, 0.6);
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 1;
+}
+
+.tui-image-cropper {
+	width: 100vw;
+	height: 100vh;
+	position: absolute;
+}
+
+.tui-content {
+	width: 100vw;
+	height: 100vh;
+	position: absolute;
+	z-index: 9;
+	display: flex;
+	flex-direction: column;
+	pointer-events: none;
+}
+
+.tui-bg-transparent {
+	background-color: rgba(0, 0, 0, 0.6);
+	transition-duration: 0.3s;
+}
+
+.tui-content-top {
+	pointer-events: none;
+}
+
+.tui-content-middle {
+	width: 100%;
+	height: 200px;
+	display: flex;
+	box-sizing: border-box;
+}
+
+.tui-cropper-box {
+	position: relative;
+	/* transition-duration: 0.2s; */
+	border-style: solid;
+	border-width: 1rpx;
+	box-sizing: border-box;
+}
+
+.tui-flex-auto {
+	flex: auto;
+}
+
+.tui-cropper-image {
+	width: 100%;
+	border-style: none;
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 2;
+	-webkit-backface-visibility: hidden;
+	backface-visibility: hidden;
+	transform-origin: center;
+}
+
+.tui-cropper-canvas {
+	position: fixed;
+	z-index: 10;
+	left: -2000px;
+	top: -2000px;
+	pointer-events: none;
+}
+
+.tui-edge {
+	border-style: solid;
+	pointer-events: auto;
+	position: absolute;
+	box-sizing: border-box;
+}
+
+.tui-top-left {
+	border-bottom-width: 0 !important;
+	border-right-width: 0 !important;
+}
+
+.tui-top-right {
+	border-bottom-width: 0 !important;
+	border-left-width: 0 !important;
+}
+
+.tui-bottom-left {
+	border-top-width: 0 !important;
+	border-right-width: 0 !important;
+}
+
+.tui-bottom-right {
+	border-top-width: 0 !important;
+	border-left-width: 0 !important;
+}
+
+.tui-cropper-tabbar {
+	width: 100%;
+	height: 120rpx;
+	padding: 0 40rpx;
+	box-sizing: border-box;
+	position: fixed;
+	left: 0;
+	bottom: 0;
+	z-index: 99;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	color: #ffffff;
+	font-size: 32rpx;
+}
+
+.tui-cropper-tabbar::after {
+	content: ' ';
+	position: absolute;
+	top: 0;
+	right: 0;
+	left: 0;
+	border-top: 1rpx solid rgba(255, 255, 255, 0.2);
+	-webkit-transform: scaleY(0.5) translateZ(0);
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 100%;
+}
+
+.tui-op-btn {
+	height: 80rpx;
+	display: flex;
+	align-items: center;
+}
+
+.tui-rotate-img {
+	width: 44rpx;
+	height: 44rpx;
+}
+</style>

+ 560 - 0
components/thorui/tui-picture-cropper/tui-picture-cropper.wxs

@@ -0,0 +1,560 @@
+var cropper = {
+	CUT_START: null,
+	cutX: 0, //画布x轴起点
+	cutY: 0, //画布y轴起点0
+	touchRelative: [{
+		x: 0,
+		y: 0
+	}], //手指或鼠标和图片中心的相对位置
+	flagCutTouch: false, //是否是拖动裁剪框
+	hypotenuseLength: 0, //双指触摸时斜边长度
+	flagEndTouch: false, //是否结束触摸
+	canvasWidth: 0,
+	canvasHeight: 0,
+	imgWidth: 0, //图片宽度
+	imgHeight: 0, //图片高度
+	scale: 1, //图片缩放比
+	angle: 0, //图片旋转角度
+	imgTop: 0, //图片上边距
+	imgLeft: 0, //图片左边距
+	//是否限制移动范围(剪裁框只能在图片内,为true不可触摸转动图片)
+	limitMove: true,
+	minHeight: 0,
+	maxHeight: 0,
+	minWidth: 0,
+	maxWidth: 0,
+	windowHeight: 0,
+	windowWidth: 0,
+	init: true
+}
+
+function bool(str) {
+	return str === 'true' || str == true ? true : false
+}
+
+function touchstart(e, ins) {
+	//var instance = e.instance;
+	// var state = instance.getState();
+	var touch = e.touches || e.changedTouches;
+	cropper.flagEndTouch = false;
+	if (touch.length == 1) {
+		cropper.touchRelative[0] = {
+			x: touch[0].pageX - cropper.imgLeft,
+			y: touch[0].pageY - cropper.imgTop
+		};
+	} else {
+		var width = Math.abs(touch[0].pageX - touch[1].pageX);
+		var height = Math.abs(touch[0].pageY - touch[1].pageY);
+		cropper.touchRelative = [{
+				x: touch[0].pageX - cropper.imgLeft,
+				y: touch[0].pageY - cropper.imgTop
+			},
+			{
+				x: touch[1].pageX - cropper.imgLeft,
+				y: touch[1].pageY - cropper.imgTop
+			}
+		];
+		cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+	}
+
+}
+
+function moveDuring(ins) {
+	if (!ins) return;
+	ins.callMethod('moveDuring')
+}
+
+function moveStop(ins) {
+	if (!ins) return;
+	ins.callMethod('moveStop')
+};
+
+function setCutCenter(ins) {
+	var cutY = (cropper.windowHeight - cropper.canvasHeight) * 0.5;
+	var cutX = (cropper.windowWidth - cropper.canvasWidth) * 0.5;
+	//顺序不能变
+	cropper.imgTop = cropper.imgTop - cropper.cutY + cutY;
+	cropper.cutY = cutY; //截取的框上边距
+	cropper.imgLeft = cropper.imgLeft - cropper.cutX + cutX;
+	cropper.cutX = cutX; //截取的框左边距
+	styleUpdate(ins)
+	cutDetectionPosition(ins)
+	imgTransform(ins)
+	updateData(ins)
+}
+
+function touchmove(e, ins) {
+	var touch = e.touches || e.changedTouches;
+	if (cropper.flagEndTouch) return;
+	moveDuring(ins);
+	if (e.touches.length == 1) {
+		var left = touch[0].pageX - cropper.touchRelative[0].x,
+			top = touch[0].pageY - cropper.touchRelative[0].y;
+		cropper.imgLeft = left;
+		cropper.imgTop = top;
+		imgTransform(ins);
+		imgMarginDetectionPosition(ins);
+	} else {
+		var res = e.instance.getDataset();
+		var minScale = +res.minscale;
+		var maxScale = +res.maxscale;
+		var disableRotate = bool(res.disablerotate)
+		var width = Math.abs(touch[0].pageX - touch[1].pageX),
+			height = Math.abs(touch[0].pageY - touch[1].pageY),
+			hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
+			scale = cropper.scale * (hypotenuse / cropper.hypotenuseLength),
+			current_deg = 0;
+		scale = scale <= minScale ? minScale : scale;
+		scale = scale >= maxScale ? maxScale : scale;
+		cropper.scale = scale;
+		imgMarginDetectionScale(ins, true);
+		var touchRelative = [{
+				x: touch[0].pageX - cropper.imgLeft,
+				y: touch[0].pageY - cropper.imgTop
+			},
+			{
+				x: touch[1].pageX - cropper.imgLeft,
+				y: touch[1].pageY - cropper.imgTop
+			}
+		];
+		if (!disableRotate) {
+			var first_atan = (180 / Math.PI) * Math.atan2(touchRelative[0].y, touchRelative[0].x);
+			var first_atan_old = (180 / Math.PI) * Math.atan2(cropper.touchRelative[0].y, cropper.touchRelative[0].x);
+			var second_atan = (180 / Math.PI) * Math.atan2(touchRelative[1].y, touchRelative[1].x);
+			var second_atan_old = (180 / Math.PI) * Math.atan2(cropper.touchRelative[1].y, cropper.touchRelative[1].x);
+			var first_deg = first_atan - first_atan_old,
+				second_deg = second_atan - second_atan_old;
+			if (first_deg != 0) {
+				current_deg = first_deg;
+			} else if (second_deg != 0) {
+				current_deg = second_deg;
+			}
+		}
+		cropper.touchRelative = touchRelative;
+		cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+		//更新视图
+		cropper.angle = cropper.angle + current_deg;
+		imgTransform(ins);
+	}
+}
+
+function touchend(e, ins) {
+	cropper.flagEndTouch = true;
+	moveStop(ins);
+	updateData(ins)
+}
+
+
+function cutTouchStart(e, ins) {
+	var touch = e.touches || e.changedTouches;
+	var currentX = touch[0].pageX;
+	var currentY = touch[0].pageY;
+
+	/*
+	 * (右下-1 右上-2 左上-3 左下-4)
+	 * left_x [3,4]
+	 * top_y [2,3]
+	 * right_x [1,2]
+	 * bottom_y [1,4]
+	 */
+	var left_x1 = cropper.cutX - 30;
+	var left_x2 = cropper.cutX + 30;
+
+	var top_y1 = cropper.cutY - 30;
+	var top_y2 = cropper.cutY + 30;
+
+	var right_x1 = cropper.cutX + cropper.canvasWidth - 30;
+	var right_x2 = cropper.cutX + cropper.canvasWidth + 30;
+
+	var bottom_y1 = cropper.cutY + cropper.canvasHeight - 30;
+	var bottom_y2 = cropper.cutY + cropper.canvasHeight + 30;
+
+	if (currentX > right_x1 && currentX < right_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
+		moveDuring();
+		cropper.flagCutTouch = true;
+		cropper.flagEndTouch = true;
+		cropper.CUT_START = {
+			width: cropper.canvasWidth,
+			height: cropper.canvasHeight,
+			x: currentX,
+			y: currentY,
+			corner: 1
+		};
+	} else if (currentX > right_x1 && currentX < right_x2 && currentY > top_y1 && currentY < top_y2) {
+		moveDuring();
+		cropper.flagCutTouch = true;
+		cropper.flagEndTouch = true;
+		cropper.CUT_START = {
+			width: cropper.canvasWidth,
+			height: cropper.canvasHeight,
+			x: currentX,
+			y: currentY,
+			cutY: cropper.cutY,
+			cutX: cropper.cutX,
+			corner: 2
+		};
+	} else if (currentX > left_x1 && currentX < left_x2 && currentY > top_y1 && currentY < top_y2) {
+		moveDuring();
+		cropper.flagCutTouch = true;
+		cropper.flagEndTouch = true;
+		cropper.CUT_START = {
+			width: cropper.canvasWidth,
+			height: cropper.canvasHeight,
+			cutY: cropper.cutY,
+			cutX: cropper.cutX,
+			x: currentX,
+			y: currentY,
+			corner: 3
+		};
+	} else if (currentX > left_x1 && currentX < left_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
+		moveDuring();
+		cropper.flagCutTouch = true;
+		cropper.flagEndTouch = true;
+		cropper.CUT_START = {
+			width: cropper.canvasWidth,
+			height: cropper.canvasHeight,
+			cutY: cropper.cutY,
+			cutX: cropper.cutX,
+			x: currentX,
+			y: currentY,
+			corner: 4
+		};
+	}
+}
+
+function cutTouchMove(e, ins) {
+	if (!cropper.CUT_START || cropper.CUT_START === 'null') return;
+	if (cropper.flagCutTouch) {
+		var touch = e.touches || e.changedTouches;
+		var res = e.instance.getDataset();
+		var lockRatio = bool(res.lockratio);
+		var lockWidth = bool(res.lockwidth);
+		var lockHeight = bool(res.lockheight);
+		if (lockRatio && (lockWidth || lockHeight)) return;
+		var width = cropper.canvasWidth,
+			height = cropper.canvasHeight,
+			cutY = cropper.cutY,
+			cutX = cropper.cutX;
+
+		var size_correct = function() {
+			width = width <= cropper.maxWidth ? (width >= cropper.minWidth ? width : cropper.minWidth) : cropper.maxWidth;
+			height = height <= cropper.maxHeight ? (height >= cropper.minHeight ? height : cropper.minHeight) : cropper.maxHeight;
+		}
+
+		var size_inspect = function() {
+			if ((width > cropper.maxWidth || width < cropper.minWidth || height > cropper.maxHeight || height < cropper.minHeight) &&
+				lockRatio) {
+				size_correct();
+				return false;
+			} else {
+				size_correct();
+				return true;
+			}
+		};
+		height = cropper.CUT_START.height + (cropper.CUT_START.corner > 1 && cropper.CUT_START.corner < 4 ? 1 : -1) * (
+			cropper.CUT_START.y - touch[0].pageY);
+		switch (cropper.CUT_START.corner) {
+			case 1:
+				width = cropper.CUT_START.width - cropper.CUT_START.x + touch[0].pageX;
+				if (lockRatio) {
+					height = width / (cropper.canvasWidth / cropper.canvasHeight);
+				}
+				if (!size_inspect()) return;
+				break;
+			case 2:
+				width = cropper.CUT_START.width - cropper.CUT_START.x + touch[0].pageX;
+				if (lockRatio) {
+					height = width / (cropper.canvasWidth / cropper.canvasHeight);
+				}
+				if (!size_inspect()) return;
+				cutY = cropper.CUT_START.cutY - (height - cropper.CUT_START.height);
+				break;
+			case 3:
+				width = cropper.CUT_START.width + cropper.CUT_START.x - touch[0].pageX;
+				if (lockRatio) {
+					height = width / (cropper.canvasWidth / cropper.canvasHeight);
+				}
+				if (!size_inspect()) return;
+				cutY = cropper.CUT_START.cutY - (height - cropper.CUT_START.height);
+				cutX = cropper.CUT_START.cutX - (width - cropper.CUT_START.width);
+				break;
+			case 4:
+				width = cropper.CUT_START.width + cropper.CUT_START.x - touch[0].pageX;
+				if (lockRatio) {
+					height = width / (cropper.canvasWidth / cropper.canvasHeight);
+				}
+				if (!size_inspect()) return;
+				cutX = cropper.CUT_START.cutX - (width - cropper.CUT_START.width);
+				break;
+			default:
+				break;
+		}
+		if (!lockWidth && !lockHeight) {
+			cropper.canvasWidth = width;
+			cropper.cutX = cutX;
+			cropper.canvasHeight = height;
+			cropper.cutY = cutY;
+			canvasHeight(ins);
+			canvasWidth(ins);
+		} else if (!lockWidth) {
+			cropper.canvasWidth = width;
+			cropper.cutX = cutX;
+			canvasWidth(ins);
+		} else if (!lockHeight) {
+			cropper.canvasHeight = height;
+			cropper.cutY = cutY;
+			canvasHeight(ins);
+		}
+		styleUpdate(ins)
+		imgMarginDetectionScale(ins);
+	}
+}
+
+//检测剪裁框位置是否在允许的范围内(屏幕内)
+function cutDetectionPosition(ins) {
+	var windowHeight = cropper.windowHeight,
+		windowWidth = cropper.windowWidth;
+
+	var cutDetectionPositionTop = function() {
+		//检测上边距是否在范围内
+		if (cropper.cutY < 0) {
+			cropper.cutY = 0;
+		}
+		if (cropper.cutY > windowHeight - cropper.canvasHeight) {
+			cropper.cutY = windowHeight - cropper.canvasHeight;
+		}
+	}
+
+	var cutDetectionPositionLeft = function() {
+		//检测左边距是否在范围内
+		if (cropper.cutX < 0) {
+			cropper.cutX = 0;
+		}
+		if (cropper.cutX > windowWidth - cropper.canvasWidth) {
+			cropper.cutX = windowWidth - cropper.canvasWidth;
+		}
+	}
+	//裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中)
+	if (cropper.cutY == null && cropper.cutX == null) {
+		var cutY = (windowHeight - cropper.canvasHeight) * 0.5;
+		var cutX = (windowWidth - cropper.canvasWidth) * 0.5;
+		cropper.cutY = cutY; //截取的框上边距
+		cropper.cutX = cutX; //截取的框左边距
+	} else if (cropper.cutY != null && cropper.cutX != null) {
+		cutDetectionPositionTop();
+		cutDetectionPositionLeft();
+	} else if (cropper.cutY != null && cropper.cutX == null) {
+		cutDetectionPositionTop();
+		cropper.cutX = (windowWidth - cropper.canvasWidth) / 2;
+	} else if (cropper.cutY == null && cropper.cutX != null) {
+		cutDetectionPositionLeft();
+		cropper.cutY = (windowHeight - cropper.canvasHeight) / 2;
+	}
+
+	styleUpdate(ins)
+}
+
+/**
+ * 图片边缘检测-缩放
+ */
+function imgMarginDetectionScale(ins, delay) {
+	if (!cropper.limitMove) return;
+	var scale = cropper.scale;
+	var imgWidth = cropper.imgWidth;
+	var imgHeight = cropper.imgHeight;
+	if ((cropper.angle / 90) % 2) {
+		imgWidth = cropper.imgHeight;
+		imgHeight = cropper.imgWidth;
+	}
+	if (imgWidth * scale < cropper.canvasWidth) {
+		scale = cropper.canvasWidth / imgWidth;
+	}
+	if (imgHeight * scale < cropper.canvasHeight) {
+		scale = Math.max(scale, cropper.canvasHeight / imgHeight);
+	}
+	imgMarginDetectionPosition(ins, scale, delay);
+}
+/**
+ * 图片边缘检测-位置
+ */
+function imgMarginDetectionPosition(ins, scale, delay) {
+	if (!cropper.limitMove) return;
+	var left = cropper.imgLeft;
+	var top = cropper.imgTop;
+	scale = scale || cropper.scale;
+	var imgWidth = cropper.imgWidth;
+	var imgHeight = cropper.imgHeight;
+	if ((cropper.angle / 90) % 2) {
+		imgWidth = cropper.imgHeight;
+		imgHeight = cropper.imgWidth;
+	}
+	left = cropper.cutX + (imgWidth * scale) / 2 >= left ? left : cropper.cutX + (imgWidth * scale) / 2;
+	left = cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2 <= left ? left : cropper.cutX + cropper.canvasWidth -
+		(imgWidth * scale) / 2;
+	top = cropper.cutY + (imgHeight * scale) / 2 >= top ? top : cropper.cutY + (imgHeight * scale) / 2;
+	top = cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2 <= top ? top : cropper.cutY + cropper.canvasHeight -
+		(imgHeight * scale) / 2;
+
+	cropper.imgLeft = left;
+	cropper.imgTop = top;
+	cropper.scale = scale;
+	styleUpdate(ins)
+	if (!delay || delay === 'null') {
+		imgTransform(ins);
+	}
+}
+
+
+function cutTouchEnd(e, ins) {
+	moveStop(ins);
+	cropper.flagCutTouch = false;
+	updateData(ins)
+}
+
+
+//改变截取框大小
+function computeCutSize(ins) {
+	if (cropper.canvasWidth > cropper.windowWidth) {
+		cropper.canvasWidth = cropper.windowWidth;
+		// canvasWidth(ins)
+	} else if (cropper.canvasWidth + cropper.cutX > cropper.windowWidth) {
+		cropper.cutX = cropper.windowWidth - cropper.cutX;
+	}
+	if (cropper.canvasHeight > cropper.windowHeight) {
+		cropper.canvasHeight = cropper.windowHeight;
+		// canvasHeight(ins)
+	} else if (cropper.canvasHeight + cropper.cutY > cropper.windowHeight) {
+		cropper.cutY = cropper.windowHeight - cropper.cutY;
+	}
+	// styleUpdate(ins)
+}
+
+function styleUpdate(ins) {
+	if (!ins) return;
+	ins.selectComponent('.tui-cropper-box').setStyle({
+		'width': cropper.canvasWidth + 'px',
+		'height': cropper.canvasHeight + 'px'
+	})
+	ins.selectComponent('.tui-content-middle').setStyle({
+		'height': cropper.canvasHeight + 'px'
+	})
+	ins.selectComponent('.tui-content-top').setStyle({
+		'height': cropper.cutY + 'px'
+	})
+	ins.selectComponent('.tui-wxs-bg').setStyle({
+		'width': cropper.cutX + 'px'
+	})
+
+}
+
+function imgTransform(ins) {
+	var owner = ins.selectComponent('.tui-cropper-image')
+	if (!owner) return
+	var x = cropper.imgLeft - cropper.imgWidth / 2;
+	var y = cropper.imgTop - cropper.imgHeight / 2;
+	owner.setStyle({
+		'transform': 'translate3d(' + x + 'px,' + y + 'px,0) scale(' + cropper.scale + ') rotate(' + cropper.angle + 'deg)'
+	})
+}
+
+function imageReset(ins) {
+	cropper.scale = 1;
+	cropper.angle = 0;
+	imgTransform(ins);
+}
+//监听截取框宽高变化
+function canvasWidth(ins) {
+	if (cropper.canvasWidth < cropper.minWidth) {
+		cropper.canvasWidth = cropper.minWidth;
+	}
+	if (!ins) return;
+	computeCutSize(ins);
+}
+
+function canvasHeight(ins) {
+	if (cropper.canvasHeight < cropper.minHeight) {
+		cropper.canvasHeight = cropper.minHeight;
+	}
+	if (!ins) return;
+	computeCutSize(ins);
+}
+
+function updateData(ins) {
+	if (!ins) return;
+	ins.callMethod('change', {
+		cutX: cropper.cutX,
+		cutY: cropper.cutY,
+		canvasWidth: cropper.canvasWidth,
+		canvasHeight: cropper.canvasHeight,
+		imgWidth: cropper.imgWidth,
+		imgHeight: cropper.imgHeight,
+		scale: cropper.scale,
+		angle: cropper.angle,
+		imgTop: cropper.imgTop,
+		imgLeft: cropper.imgLeft
+	})
+}
+
+function propsChange(prop, oldProp, ownerInstance, ins) {
+	if (prop && prop !== 'null') {
+		var params = prop.split(',')
+		var type = +params[0]
+		var dataset = ins.getDataset();
+		if (cropper.init || type == 4) {
+			cropper.maxHeight = +dataset.maxheight;
+			cropper.minHeight = +dataset.minheight;
+			cropper.maxWidth = +dataset.maxwidth;
+			cropper.minWidth = +dataset.minwidth;
+			cropper.canvasWidth = +dataset.width;
+			cropper.canvasHeight = +dataset.height;
+			cropper.imgTop = dataset.windowheight / 2;
+			cropper.imgLeft = dataset.windowwidth / 2;
+			cropper.imgWidth = +dataset.imgwidth;
+			cropper.imgHeight = +dataset.imgheight;
+			cropper.windowHeight = +dataset.windowheight;
+			cropper.windowWidth = +dataset.windowwidth;
+			cropper.init = false
+		} else if (type == 2 || type == 3) {
+			cropper.imgWidth = +dataset.imgwidth;
+			cropper.imgHeight = +dataset.imgheight;
+		}
+		cropper.limitMove = bool(dataset.limitmove);
+		cropper.angle = +dataset.angle;
+		if (type == 3) {
+			imgTransform(ownerInstance);
+		}
+		switch (type) {
+			case 1:
+				setCutCenter(ownerInstance);
+				//设置裁剪框大小>设置图片尺寸>绘制canvas
+				computeCutSize(ownerInstance);
+				//检查裁剪框是否在范围内
+				cutDetectionPosition(ownerInstance);
+				break;
+			case 2:
+				setCutCenter(ownerInstance);
+				break;
+			case 3:
+				imgMarginDetectionScale(ownerInstance)
+				break;
+			case 4:
+				imageReset(ownerInstance);
+				break;
+			case 5:
+				setCutCenter(ownerInstance);
+				break;
+			default:
+				break;
+		}
+	}
+}
+
+module.exports = {
+	touchstart: touchstart,
+	touchmove: touchmove,
+	touchend: touchend,
+	cutTouchStart: cutTouchStart,
+	cutTouchMove: cutTouchMove,
+	cutTouchEnd: cutTouchEnd,
+	propsChange: propsChange
+}

+ 161 - 0
components/thorui/tui-rate/tui-rate.vue

@@ -0,0 +1,161 @@
+<template>
+	<view class="tui-rate-class tui-rate-box" @touchmove="touchMove">
+		<block v-for="(item,index) in quantity" :key="index">
+			<view class="tui-icon tui-relative" :class="['tui-icon-collection'+(hollow && (current<=index || (disabled && current<=index+1))?'':'-fill')]"
+			 :data-index="index" @tap="handleTap" :style="{fontSize:size+'px',color:(current>index+1 || (!disabled && current>index))?active:normal}">
+				<view class="tui-icon tui-icon-main tui-icon-collection-fill" v-if="disabled && current==index+1" :style="{fontSize:size+'px',color:active,width:percent+'%'}"></view>
+			</view>
+		</block>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiRate",
+		props: {
+			//数量
+			quantity: {
+				type: Number,
+				default: 5
+			},
+			//当前选中
+			current: {
+				type: Number,
+				default: 0
+			},
+			//当前选中星星分数(大于0,小于等于1的数)
+			score: {
+				type: [Number, String],
+				default: 1
+			},
+			//禁用点击
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//大小
+			size: {
+				type: Number,
+				default: 20
+			},
+			//未选中颜色
+			normal: {
+				type: String,
+				default: "#b2b2b2"
+			},
+			//选中颜色
+			active: {
+				type: String,
+				default: "#e41f19"
+			},
+			//未选中是否为空心
+			hollow: {
+				type: Boolean,
+				default: false
+			},
+			//自定义参数
+			params: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		data() {
+			return {
+				pageX: 0,
+				percent: 0
+			};
+		},
+		created() {
+			this.percent = Number(this.score || 0) * 100
+		},
+		watch: {
+			score(newVal, oldVal) {
+				this.percent = Number(newVal || 0) * 100
+			}
+		},
+		methods: {
+			handleTap(e) {
+				if (this.disabled) {
+					return;
+				}
+				const index = e.currentTarget.dataset.index;
+				this.$emit('change', {
+					index: Number(index) + 1,
+					params:this.params
+				})
+			},
+			touchMove(e) {
+				if (this.disabled) {
+					return;
+				}
+				if (!e.changedTouches[0]) {
+					return;
+				}
+				const movePageX = e.changedTouches[0].pageX;
+				const distance = movePageX - this.pageX;
+
+				if (distance <= 0) {
+					return;
+				}
+				let index = Math.ceil(distance / this.size);
+				index = index > this.count ? this.count : index;
+				this.$emit('change', {
+					index: index,
+					params:this.params
+				})
+			}
+		},
+		mounted() {
+			const className = '.tui-rate-box';
+			let query = uni.createSelectorQuery().in(this)
+			query.select(className).boundingClientRect((res) => {
+				this.pageX = res.left || 0
+			}).exec()
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'rateFont';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAT4AA0AAAAAB4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAE3AAAABoAAAAciBprQUdERUYAAAS8AAAAHgAAAB4AKQALT1MvMgAAAaAAAABDAAAAVj1YSN1jbWFwAAAB+AAAAEIAAAFCAA/qlmdhc3AAAAS0AAAACAAAAAj//wADZ2x5ZgAAAkgAAADwAAABZLMTdXtoZWFkAAABMAAAADAAAAA2FZKISmhoZWEAAAFgAAAAHQAAACQHYgOFaG10eAAAAeQAAAARAAAAEgx6AHpsb2NhAAACPAAAAAwAAAAMAEYAsm1heHAAAAGAAAAAHgAAACABEQBPbmFtZQAAAzgAAAFJAAACiCnmEVVwb3N0AAAEhAAAAC0AAABHLO3vkXjaY2BkYGAA4t2/VF7G89t8ZeBmYQCBm9ZKMnC6ikGMuYXpP5DLwcAEEgUAHPQJOXjaY2BkYGBu+N/AEMPCAALMLQyMDKiABQBQwgLwAAAAeNpjYGRgYGBlcGZgYgABEMkFhAwM/8F8BgAPigFhAAB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PXj17zdzwv4EhhrmBoQEozAiSAwD/YA2wAHjaY2GAABYIrmKoAgACggEBAAAAeNpjYGBgZoBgGQZGBhCwAfIYwXwWBgUgzQKEQP6z1///A8lX//9LSkJVMjCyMcCYDIxMQIKJARUwMgx7AAA/9QiLAAAAAAAAAAAAAABGALJ42mNgZKhiEGNuYfrPoMnAwGimps+ox6jPqKbEz8jHCMLyjHJAmk1czMie0cxInlHMDChrZs6cJyaosI+NlzmU34I/lImPdb+CoHgXCyujIosYtzTfKlYBtlWyuqwKjKwsjNvFTdlkGDnZ1srKrmXjZJRhMxVvZxFgA+rgYI9iYoriV1TYzybAwsDABHeLBIMT0DUg29VBTjEHucvcjtGeUVyOUZ6JaFcybefnZ5HuFdEX6ZVm5uMvniemxuXmzqUmNs+FeOfHCeiKzfPi4vKaJ6YrUCDOIiM8YYKwDIu4OMRbrOtkZdex4vMWACzGM5B42n2QPU4DMRCFn/MHJBJCIKhdUQDa/JQpEyn0CKWjSDbekGjXXnmdSDkBLRUHoOUYHIAbINFyCl6WSZMia+3o85uZ57EBnOMbCv/fJe6EFY7xKFzBETLhKvUX4Rr5XbiOFj6FG9R/hJu4VQPhFi7UGx1U7YS7m9JtywpnGAhXcIon4Sr1lXCN/CpcxxU+hBvUv4SbGONXuIVrZakM4WEwQWCcQWOKDeMCMRwskjIG1qE59GYSzExPN3oRO5s4GyjvV2KXAx5oOeeAKe09t2a+Sif+YMuB1JhuHgVLtimNLiJ0KBtfLJzV3ahzsP2e7ba02L9rgTXH7FENbNT8Pdsz0khsDK+QkjXyMrekElOPaGus8btnKdbzXgiJTrzL9IjHmjR1OvduaeLA4ufyjBx9tLmSPfeoHD5jWQh5v91OxCCKXYY/k9hxGQAAAHjaY2BigAAuMMnIgA5YwaJMjEyMzPzJ+Tk5qcklmfl58WmZOTlcCD4Ak9QKlAAAAAAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAQAAQAEAAAAAgAAAAB42mNgYGBkAIKrS9Q5QPRNayUZGA0AM8UETgAA) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-icon {
+		font-family: "rateFont" !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+		display: block;
+	}
+
+	.tui-relative {
+		position: relative;
+	}
+
+	.tui-icon-main {
+		position: absolute;
+		height: 100%;
+		left: 0;
+		top: 0;
+		overflow: hidden;
+	}
+
+	.tui-icon-collection-fill:before {
+		content: "\e6ea";
+	}
+
+	.tui-icon-collection:before {
+		content: "\e6eb";
+	}
+
+	.tui-rate-box {
+		display: -webkit-inline-flex;
+		display: inline-flex;
+		align-items: center;
+		margin: 0;
+		padding: 0;
+	}
+</style>

+ 307 - 0
components/thorui/tui-round-progress/tui-round-progress.vue

@@ -0,0 +1,307 @@
+<template>
+	<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
+		<canvas
+			:start="percent"
+			:change:start="parse.initDraw"
+			:data-width="diam"
+			:data-height="height"
+			:data-lineWidth="lineWidth"
+			:data-lineCap="lineCap"
+			:data-fontSize="fontSize"
+			:data-fontColor="fontColor"
+			:data-fontShow="fontShow"
+			:data-percentText="percentText"
+			:data-defaultShow="defaultShow"
+			:data-defaultColor="defaultColor"
+			:data-progressColor="progressColor"
+			:data-gradualColor="gradualColor"
+			:data-sAngle="sAngle"
+			:data-counterclockwise="counterclockwise"
+			:data-multiple="multiple"
+			:data-speed="speed"
+			:data-activeMode="activeMode"
+			:data-cid="progressCanvasId"
+			:canvas-id="progressCanvasId"
+			:class="[progressCanvasId]"
+			:style="{ width: diam + 'px', height: (height || diam) + 'px' }"
+		></canvas>
+		<slot></slot>
+	</view>
+</template>
+<script module="parse" lang="renderjs">
+ export default {
+	methods:{
+		format(str){
+			if(!str) return str;
+			return str.replace(/\"/g, "");
+		},
+		bool(str){
+			return str==='true' || str==true ? true:false
+		},
+		//初始化绘制
+		  initDraw(percentage, oldPercentage, owner, ins) {
+			 let state=ins.getState();
+			 let res=ins.getDataset();
+			 const activeMode=this.format(res.activemode);
+			 let start = activeMode === 'backwards' ? 0 : (state.startPercentage || 0);
+			 if(!state.progressContext || !state.canvas){
+				const width = res.width;
+				const height = res.height==0?res.width:res.height;
+				let ele=`.${res.cid}>canvas`
+				const canvas = document.querySelectorAll(this.format(ele))[0];
+				const ctx = canvas.getContext('2d');
+				// const dpr =uni.getSystemInfoSync().pixelRatio;
+				// canvas.style.width=width+'px';
+				// canvas.style.height=height+'px';
+				// canvas.width = width * dpr;
+				// canvas.height = height * dpr;
+				// ctx.scale(dpr, dpr);
+				state.progressContext=ctx;
+				state.canvas=canvas;
+				this.drawProgressCircular(start, ctx, canvas,percentage,res,state,owner);
+			 }else{
+		         this.drawProgressCircular(start, state.progressContext, state.canvas,percentage,res,state,owner);
+			 }
+		  },
+		  //默认(背景)圆环
+		  drawDefaultCircular(ctx, canvas,res) {
+		    //终止弧度
+			let sangle=Number(res.sangle) * Math.PI
+		    let eAngle = Math.PI * (res.height!=0 ? 1 : 2) + sangle;
+		    this.drawArc(ctx, eAngle,this.format(res.defaultcolor),res);
+		  },
+		  drawPercentage(ctx, percentage,res) {
+		    ctx.save(); //save和restore可以保证样式属性只运用于该段canvas元素
+		    ctx.beginPath();
+		    ctx.fillStyle = this.format(res.fontcolor);
+		    ctx.font = res.fontsize + "px Arial"; //设置字体大小和字体
+		    ctx.textAlign = "center";
+		    ctx.textBaseline = "middle";
+		    let radius = res.width / 2;
+			let percenttext=this.format(res.percenttext)
+		    if (!percenttext) {
+			  let multiple=Number(res.multiple)
+		      percentage = this.bool(res.counterclockwise) ? 100 - percentage * multiple : percentage * multiple;
+		      percentage = percentage.toFixed(0) + "%"
+		    } else {
+		      percentage = percenttext
+		    }
+		    ctx.fillText(percentage, radius, radius);
+		    ctx.stroke();
+		    ctx.restore();
+		  },
+		  //进度圆环
+		  drawProgressCircular(startPercentage, ctx, canvas,percentage,res,state,owner) {
+		    if (!ctx || !canvas) return;
+			let that=this
+		    let gradient = ctx.createLinearGradient(0, 0, Number(res.width), 0);
+		    gradient.addColorStop(0, this.format(res.progresscolor));
+			let gradualColor=this.format(res.gradualcolor)
+		    if (gradualColor) {
+		      gradient.addColorStop('1', gradualColor);
+		    }
+		    let requestId = null
+		    let renderLoop = () => {
+		      drawFrame((res) => {
+		        if (res) {
+		          requestId = requestAnimationFrame(renderLoop)
+		        } else {
+		          cancelAnimationFrame(requestId)
+		          requestId = null;
+		          renderLoop = null;
+		        }
+		      })
+		    }
+		    requestId = requestAnimationFrame(renderLoop)
+
+		    function drawFrame(callback) {
+		      ctx.clearRect(0, 0, canvas.width, canvas.height);
+		      if (that.bool(res.defaultshow)) {
+		        that.drawDefaultCircular(ctx, canvas,res)
+		      }
+		      if (that.bool(res.fontshow)) {
+		        that.drawPercentage(ctx, startPercentage,res);
+		      }
+		      if (percentage === 0 || (that.bool(res.counterclockwise) && startPercentage === 100)) return;
+		      let sangle=Number(res.sangle) * Math.PI
+			  let eAngle = ((2 * Math.PI) / 100) * startPercentage + sangle;
+			 that.drawArc(ctx, eAngle, gradient,res);
+			  owner.callMethod('change', {
+				percentage:startPercentage
+			 })
+		      if (startPercentage >= percentage) {
+				state.startPercentage=startPercentage;
+				owner.callMethod('end', {
+					canvasId: that.format(res.canvasid)
+				})
+		        cancelAnimationFrame(requestId)
+		        callback && callback(false)
+		        return;
+		      }
+		      let num = startPercentage + Number(res.speed)
+		      startPercentage = num > percentage ? percentage : num;
+		      callback && callback(true)
+		    }
+
+		  },
+		  //创建弧线
+		  drawArc(ctx, eAngle, strokeStyle,res) {
+		    ctx.save();
+		    ctx.beginPath();
+		    ctx.lineCap = this.format(res.linecap);
+		    ctx.lineWidth =Number(res.linewidth);
+		    ctx.strokeStyle = strokeStyle;
+		    let radius = res.width / 2; //x=y
+			let sangle=Number(res.sangle) * Math.PI
+		    ctx.arc(radius, radius, radius - res.linewidth, sangle, eAngle,this.bool(res.counterclockwise));
+		    ctx.stroke();
+		    ctx.closePath();
+		    ctx.restore();
+		  }
+	}
+}
+</script>
+<script>
+export default {
+	name: 'tuiRoundProgress',
+	props: {
+		/*
+		  传值需使用rpx进行转换保证各终端兼容
+		  px = rpx / 750 * wx.getSystemInfoSync().windowWidth
+		  圆形进度条(画布)宽度,直径 [px]
+		*/
+		diam: {
+			type: Number,
+			default: 60
+		},
+		//圆形进度条(画布)高度,默认取diam值[当画半弧时传值,height有值时则取height]
+		height: {
+			type: Number,
+			default: 0
+		},
+		//进度条线条宽度[px]
+		lineWidth: {
+			type: Number,
+			default: 4
+		},
+		/*
+			 线条的端点样式
+			 butt:向线条的每个末端添加平直的边缘
+			 round	向线条的每个末端添加圆形线帽
+			 square	向线条的每个末端添加正方形线帽
+			*/
+		lineCap: {
+			type: String,
+			default: 'round'
+		},
+		//圆环进度字体大小 [px]
+		fontSize: {
+			type: Number,
+			default: 12
+		},
+		//圆环进度字体颜色
+		fontColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//是否显示进度文字
+		fontShow: {
+			type: Boolean,
+			default: true
+		},
+		/*
+			 自定义显示文字[默认为空,显示百分比,fontShow=true时生效]
+			 可以使用 slot自定义显示内容
+			*/
+		percentText: {
+			type: String,
+			default: ''
+		},
+		//是否显示默认(背景)进度条
+		defaultShow: {
+			type: Boolean,
+			default: true
+		},
+		//默认进度条颜色
+		defaultColor: {
+			type: String,
+			default: '#CCC'
+		},
+		//进度条颜色
+		progressColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//进度条渐变颜色[结合progressColor使用,默认为空]
+		gradualColor: {
+			type: String,
+			default: ''
+		},
+		//起始弧度,单位弧度 实际  Math.PI * sAngle
+		sAngle: {
+			type: Number,
+			default: -0.5
+		},
+		//指定弧度的方向是逆时针还是顺时针。默认是false,即顺时针
+		counterclockwise: {
+			type: Boolean,
+			default: false
+		},
+		//进度百分比 [10% 传值 10]
+		percentage: {
+			type: Number,
+			default: 0
+		},
+		//进度百分比缩放倍数[使用半弧为100%时,则可传2]
+		multiple: {
+			type: Number,
+			default: 1
+		},
+		//动画执行速度,值越大动画越快(0.1~100)
+		speed: {
+			type: [Number, String],
+			default: 1
+		},
+		//backwards: 动画从头播;forwards:动画从上次结束点接着播
+		activeMode: {
+			type: String,
+			default: 'backwards'
+		}
+	},
+	watch: {
+		percentage(val) {
+			this.percent = val;
+		}
+	},
+	mounted() {
+		setTimeout(() => {
+			this.percent = this.percentage;
+		}, 50);
+	},
+	data() {
+		return {
+			percent: -1,
+			progressCanvasId: this.getCanvasId()
+		};
+	},
+	methods: {
+		getCanvasId() {
+			return 'tui' + new Date().getTime() + (Math.random() * 100000).toFixed(0);
+		},
+		change(e) {
+			//绘制进度
+			this.$emit('change', e);
+		},
+		end(e) {
+			//绘制完成
+			this.$emit('end', e);
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-circular-container {
+	position: relative;
+}
+</style>

+ 178 - 0
components/thorui/tui-scroll-top/tui-scroll-top.vue

@@ -0,0 +1,178 @@
+<template>
+	<view class="tui-scroll-top_box" v-show="isIndex || isShare || (visible && toggle)" :style="{ bottom: bottom + 'rpx', right: right + 'rpx' }">
+		<view class="tui-scroll-top_item" v-if="isIndex" @tap.stop="goIndex">
+			<image class="tui-scroll-top_img" :src="indexIcon"></image>
+			<view class="tui-scroll-top_text">首页</view>
+		</view>
+		<button open-type="share" class="tui-share-btn" v-if="isShare && !customShare">
+			<view class="tui-scroll-top_item" :class="{ 'tui-scroll-item_top': isIndex }"><image class="tui-scroll-top_img" :src="shareIcon"></image></view>
+		</button>
+		<view class="tui-scroll-top_item" :class="{ 'tui-scroll-item_top': isIndex }" v-if="isShare && customShare" @tap.stop="share">
+			<image class="tui-scroll-top_img" :src="shareIcon"></image>
+		</view>
+		<view class="tui-scroll-top_item" :class="{ 'tui-scroll-item_top': isIndex || isShare }" v-show="visible && toggle" @tap.stop="goTop">
+			<image class="tui-scroll-top_img" :src="topIcon"></image>
+			<view class="tui-scroll-top_text tui-color-white">顶部</view>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 注意:组件中使用的图片地址,将文件复制到自己项目中
+ * 如果图片位置与组件同级,编译成小程序时图片会丢失
+ * 拷贝static下整个components文件夹
+ * 也可直接转成base64(不建议)
+ * */
+export default {
+	name: 'tuiScrollTop',
+	props: {
+		//回顶部按钮距离底部距离 rpx
+		bottom: {
+			type: Number,
+			default: 180
+		},
+		//回顶部按钮距离右侧距离 rpx
+		right: {
+			type: Number,
+			default: 25
+		},
+		//距离顶部多少距离显示 px
+		top: {
+			type: Number,
+			default: 200
+		},
+		//滚动距离
+		scrollTop: {
+			type: Number
+		},
+		//回顶部滚动时间
+		duration: {
+			type: Number,
+			default: 120
+		},
+		//是否显示返回首页按钮
+		isIndex: {
+			type: Boolean,
+			default: false
+		},
+		//是否显示分享图标
+		isShare: {
+			type: Boolean,
+			default: false
+		},
+		//自定义分享(小程序可使用button=>open-type="share")
+		customShare: {
+			type: Boolean,
+			default: false
+		},
+		//回顶部icon
+		topIcon: {
+			type: String,
+			default: '/static/components/scroll-top/icon_top_3x.png'
+		},
+		//回首页icon
+		indexIcon: {
+			type: String,
+			default: '/static/components/scroll-top/icon_index_3x.png'
+		},
+		//分享icon
+		shareIcon: {
+			type: String,
+			default: '/static/components/scroll-top/icon_share_3x.png'
+		}
+	},
+	watch: {
+		scrollTop(newValue, oldValue) {
+			this.change();
+		}
+	},
+	data() {
+		return {
+			//判断是否显示
+			visible: false,
+			//控制显示,主要解决调用api滚到顶部fixed元素抖动的问题
+			toggle: true
+		};
+	},
+	methods: {
+		goTop: function() {
+			this.toggle = false;
+			uni.pageScrollTo({
+				scrollTop: 0,
+				duration: this.duration
+			});
+			setTimeout(() => {
+				this.toggle = true;
+			}, 220);
+		},
+		goIndex: function() {
+			this.$emit('index', {});
+		},
+		share() {
+			this.$emit('share', {});
+		},
+		change() {
+			let show = this.scrollTop > this.top;
+			if ((show && this.visible) || (!show && !this.visible)) {
+				return;
+			}
+			this.visible = show;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-scroll-top_box {
+	width: 80rpx;
+	height: 270rpx;
+	position: fixed;
+	z-index: 9999;
+	right: 30rpx;
+	bottom: 30rpx;
+	font-weight: 400;
+}
+
+.tui-scroll-top_item {
+	width: 80rpx;
+	height: 80rpx;
+	position: relative;
+}
+
+.tui-scroll-item_top {
+	margin-top: 30rpx;
+}
+
+.tui-scroll-top_img {
+	width: 80rpx;
+	height: 80rpx;
+	display: block;
+}
+
+.tui-scroll-top_text {
+	width: 80rpx;
+	text-align: center;
+	font-size: 24rpx;
+	line-height: 24rpx;
+	transform: scale(0.92);
+	transform-origin: center center;
+	position: absolute;
+	left: 0;
+	bottom: 15rpx;
+}
+
+.tui-color-white {
+	color: #fff;
+}
+.tui-share-btn {
+	background: transparent !important;
+	padding: 0;
+	margin: 0;
+	display: inline;
+	border: 0;
+}
+.tui-share-btn::after {
+	border: 0;
+}
+</style>

+ 239 - 0
components/thorui/tui-skeleton/tui-skeleton.vue

@@ -0,0 +1,239 @@
+<template>
+	<view class="tui-skeleton-cmomon tui-skeleton-box" :style="{width: winWidth+'px', height:winHeight+'px', backgroundColor:backgroundColor}">
+		<view class="tui-skeleton-cmomon" v-for="(item,index) in skeletonElements" :key="index" :style="{width: item.width+'px', height:item.height+'px', left: item.left+'px', top: item.top+'px',backgroundColor: skeletonBgColor,borderRadius:getRadius(item.skeletonType,borderRadius)}"></view>
+		<view class="tui-loading" :class="[getLoadingType(loadingType)]" v-if="isLoading"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiSkeleton",
+		props: {
+			//选择器(外层容器)
+			selector: {
+				type: String,
+				default: "tui-skeleton"
+			},
+			//外层容器背景颜色
+			backgroundColor: {
+				type: String,
+				default: "#fff"
+			},
+			//骨架元素背景颜色
+			skeletonBgColor: {
+				type: String,
+				default: "#e9e9e9"
+			},
+			//骨架元素类型:矩形,圆形,带圆角矩形["rect","circular","fillet"]
+			//默认所有,根据页面情况进行传值
+			//页面对应元素class为:tui-skeleton-rect,tui-skeleton-circular,tui-skeleton-fillet
+			//如果传入的值不在下列数组中,则为自定义class值,默认按矩形渲染
+			skeletonType: {
+				type: Array,
+				default () {
+					return ["rect", "circular", "fillet"]
+				}
+			},
+			//圆角值,skeletonType=fillet时生效
+			borderRadius: {
+				type: String,
+				default: "16rpx"
+			},
+			//骨架屏预生成数据:提前生成好的数据,当传入该属性值时,则不会再次查找子节点信息
+			preloadData: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//是否需要loading
+			isLoading: {
+				type: Boolean,
+				default: true
+			},
+			//loading类型[1-10]
+			loadingType: {
+				type: Number,
+				default: 1
+			}
+		},
+		created() {
+			const res = uni.getSystemInfoSync();
+			this.winWidth = res.windowWidth;
+			this.winHeight = res.windowHeight;
+			//如果有预生成数据,则直接使用
+			this.isPreload(true)
+		},
+		mounted() {
+			this.$nextTick(() => {
+				this.nodesRef(`.${this.selector}`).then((res) => {
+					if(res && res[0]){
+						this.winHeight = res[0].height + Math.abs(res[0].top)
+					}
+				});
+				!this.isPreload() && this.selectorQuery()
+			})
+
+		},
+		data() {
+			return {
+				winWidth: 375,
+				winHeight: 800,
+				skeletonElements: []
+			};
+		},
+		methods: {
+			getLoadingType: function(type) {
+				let value = 1
+				if (type && type > 0 && type < 11) {
+					value = type
+				}
+				return 'tui-loading-' + value
+			},
+			getRadius: function(type, val) {
+				let radius = "0"
+				if (type == "circular") {
+					radius = "50%"
+				} else if (type == "fillet") {
+					radius = val
+				}
+				return radius;
+			},
+			isPreload(init) {
+				let preloadData = this.preloadData || []
+				if (preloadData.length) {
+					init && (this.skeletonElements = preloadData)
+					return true
+				}
+				return false
+			},
+			async selectorQuery() {
+				let skeletonType = this.skeletonType || []
+				let nodes = []
+				for (let item of skeletonType) {
+					let className = '';
+					// #ifndef MP-WEIXIN
+					className = `.${item}`;
+					if (~'rect_circular_fillet'.indexOf(item)) {
+						className = `.${this.selector}-${item}`;
+					}
+					// #endif
+					
+					// #ifdef MP-WEIXIN
+					className = `.${this.selector} >>> .${item}`;
+					if (~'rect_circular_fillet'.indexOf(item)) {
+						className = `.${this.selector} >>> .${this.selector}-${item}`;
+					}
+					// #endif
+					await this.nodesRef(className).then((res) => {
+						res.map(d => {
+							d.skeletonType = item
+						})
+						nodes = nodes.concat(res)
+					})
+				}
+				this.skeletonElements = nodes
+			},
+			async nodesRef(className) {
+				return await new Promise((resolve, reject) => {
+					uni.createSelectorQuery().selectAll(className).boundingClientRect((res) => {
+						if (res) {
+							resolve(res);
+						} else {
+							reject(res)
+						}
+					}).exec();
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-skeleton-cmomon {
+		position: absolute;
+		z-index: 99999;
+	}
+
+	.tui-skeleton-box {
+		left: 0;
+		top: 0;
+	}
+
+	.tui-loading {
+		display: inline-block;
+		vertical-align: middle;
+		width: 40rpx;
+		height: 40rpx;
+		background: 0 0;
+		border-radius: 50%;
+		border: 2px solid;
+		animation: tui-rotate 0.7s linear infinite;
+		position: fixed;
+		z-index: 999999;
+		left: 50%;
+		top: 50%;
+		margin-left: -20rpx;
+		margin-top: -20rpx;
+	}
+
+	.tui-loading-1 {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #5677fc;
+	}
+
+	.tui-loading-2 {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
+	}
+
+	.tui-loading-3 {
+		border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) #fff;
+	}
+
+	.tui-loading-4 {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #35b06a;
+	}
+
+	.tui-loading-5 {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #fc872d;
+	}
+
+	.tui-loading-6 {
+		border-color: #e5e5e5 #e5e5e5 #e5e5e5 #eb0909;
+	}
+
+	.tui-loading-7 {
+		border-color: #5677fc transparent #5677fc transparent;
+	}
+
+	.tui-loading-8 {
+		border-color: #35b06a transparent #35b06a transparent;
+	}
+
+	.tui-loading-9 {
+		border-color: #fc872d transparent #fc872d transparent;
+	}
+
+	.tui-loading-10 {
+		border-color: #eb0909 transparent #eb0909 transparent;
+	}
+
+	@-webkit-keyframes tui-rotate {
+		0% {
+			transform: rotate(0);
+		}
+
+		100% {
+			transform: rotate(360deg);
+		}
+	}
+
+	@keyframes tui-rotate {
+		0% {
+			transform: rotate(0);
+		}
+
+		100% {
+			transform: rotate(360deg);
+		}
+	}
+</style>

+ 217 - 0
components/thorui/tui-slide-verify/tui-slide-verify.vue

@@ -0,0 +1,217 @@
+<template>
+	<view class="tui-slide-vcode" :style="{width:slideBarWidth+'px',height:slideBlockWidth+'px',backgroundColor:backgroundColor}">
+		<text class="tui-text-flashover" :style="{fontSize:size+'rpx',background:getBgColor}">拖动滑块验证</text>
+		<view class="tui-slide-glided" :style="{backgroundColor:activeBgColor}">
+			<text :style="{fontSize:size+'rpx',color:activeColor}" v-if="isPass">{{passText}}</text>
+		</view>
+		<view class="tui-slider-block" :style="{width:slideBlockWidth+'px',height:slideBlockWidth+'px',borderColor:isPass?activeBorderColor: borderColor}"
+		 :change:prop="parse.slidereset" :prop="reset" :data-slideBarWidth="slideBarWidth" :data-slideBlockWidth="slideBlockWidth"
+		 :data-errorRange="errorRange" :data-disabled="disabled" @touchstart="parse.touchstart" @touchmove="parse.touchmove"
+		 @touchend="parse.touchend">
+			<text class="tui-slide-icon tui-icon-double_arrow" :style="{fontSize:iconSize+'rpx',color:arrowColor}" v-if="!isPass"></text>
+			<text class="tui-slide-icon tui-icon-check_mark" :style="{fontSize:iconSize+'rpx',color:checkColor}" v-if="isPass"></text>
+		</view>
+	</view>
+</template>
+<script src="./tui-slide-verify.wxs" module="parse" lang="wxs"></script>
+<script>
+	export default {
+		name: "tuiSlideVerify",
+		props: {
+			//滑动条宽度 px
+			slideBarWidth: {
+				type: [Number, String],
+				default: 300
+			},
+			//滑块宽度 px = 滑动条高度
+			slideBlockWidth: {
+				type: [Number, String],
+				default: 40
+			},
+			//滑块border颜色
+			borderColor: {
+				type: String,
+				default: '#E9E9E9'
+			},
+			//通过验证后滑块border颜色
+			activeBorderColor: {
+				type: String,
+				default: '#19be6b'
+			},
+			//误差范围 px 距离右侧多少距离验证通过
+			errorRange: {
+				type: [Number, String],
+				default: 2
+			},
+			//重置滑动
+			resetSlide: {
+				type: Number,
+				default: 0
+			},
+			//提示文字大小
+			size: {
+				type: Number,
+				default: 30
+			},
+			//提示文字颜色
+			color: {
+				type: String,
+				default: "#444"
+			},
+			//验证通过后提示文字颜色
+			activeColor: {
+				type: String,
+				default: "#fff"
+			},
+			//图标字体大小 rpx
+			iconSize: {
+				type: Number,
+				default: 32
+			},
+			//箭头图标颜色
+			arrowColor: {
+				type: String,
+				default: "#cbcbcb"
+			},
+			checkColor: {
+				type: String,
+				default: "#19be6b"
+			},
+			//滑动条背景色
+			backgroundColor: {
+				type: String,
+				default: "#E9E9E9"
+			},
+			//滑过区域背景颜色
+			activeBgColor: {
+				type: String,
+				default: "#19be6b"
+			},
+			//通过提示文字
+			passText: {
+				type: String,
+				default: '验证通过'
+			}
+
+		},
+		computed: {
+			getBgColor() {
+				return `-webkit-gradient(linear, left top, right top, color-stop(0, ${this.color}), color-stop(.4, ${this.color}), color-stop(.5, white), color-stop(.6, ${this.color}), color-stop(1, ${this.color}))`
+			}
+		},
+		watch: {
+			resetSlide(val) {
+				if (val > 0) {
+					this.slideReset()
+				}
+			}
+		},
+		data() {
+			return {
+				isPass: false,
+				disabled: false,
+				reset: 0
+			}
+		},
+		methods: {
+			success() {
+				//验证成功
+				this.isPass = true;
+				this.disabled = true;
+				this.$emit('success', {})
+			},
+			slideReset() {
+				this.isPass = false;
+				this.disabled = false;
+				this.reset++;
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'tuiSlideVcode';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUYAA0AAAAAB1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAE/AAAABoAAAAci6lfG0dERUYAAATcAAAAHgAAAB4AKQALT1MvMgAAAaAAAABCAAAAVjxuSCZjbWFwAAAB+AAAAEUAAAFK5n3pi2dhc3AAAATUAAAACAAAAAj//wADZ2x5ZgAAAkwAAAD8AAABJDQ/n7JoZWFkAAABMAAAADAAAAA2GSR8FGhoZWEAAAFgAAAAHQAAACQHygOFaG10eAAAAeQAAAARAAAAEgwUAD5sb2NhAAACQAAAAAwAAAAMAFQAkm1heHAAAAGAAAAAHgAAACABEQA6bmFtZQAAA0gAAAFJAAACiCnmEVVwb3N0AAAElAAAAD0AAABPYJEgVXjaY2BkYGAA4oqPSw3j+W2+MnCzMIDAbaY5nHBa5P905jfMeUAuBwMTSBQAHycKCHjaY2BkYGBu+N/AEMPCAALMbxgYGVABCwBYegNYAAAAeNpjYGRgYGBl0GNgYgABEMkFhAwM/8F8BgANfQFMAAB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PGJ6ZMDf8b2CIYW5gaAAKM4LkAN6ZDA8AAHjaY2GAABYItmMQAQABaABfAAAAeNpjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYnjE8M/n/n4EBQksxS16AqgcCRjYGOIeRCUgwMaACRoZhDwDR6wnSAAAAAAAAAAAAAAAAVACSeNpFzjFOwzAYxfHv2Yodu4ozxHEq2qoSEilLQYoqh6lIIBaugMTC3hswMcPQhYmBjV4AMSFxAppjQDmDSzJle9L7DX9itNx/8i9+QY7mRPDn8ItTlDOcQLhCwcBVtWLCOl/D10v0L5vHnAGMx+EuLSctvQ8PBpMyxWU30/GxwUvMwXqDW6lkNIikllgnGM1MeAqPyWxkeNktczRGgrXUXOkeETGy+2f+x1c0oGnbKUg6KjzVJWUQh23TwlfTrhW+cpZRE3ZCIG8a5EKE3U34yM/sRttCb5hiuNLDjK+i8PO9Db8igmu2cOE1vNsWDTP9xhiuVXZARP+yvTqbeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMGiTIxMjMyCOalpJbop+aVJOam6iUVF+eUCKaWZ6fmlJZmJeckZ+XnpugDvDw1eAAAAAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABAABAAQAAAACAAAAAHjaY2BgYGQAgqtL1DlA9G2mOZwwGgA1wQSuAAA=) format('woff');
+		font-weight: normal;
+		font-style: normal;
+		font-display: swap;
+	}
+
+	.tui-slide-icon {
+		font-family: "tuiSlideVcode" !important;
+		font-size: 34rpx;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-icon-check_mark:before {
+		content: "\e634";
+	}
+
+	.tui-icon-double_arrow:before {
+		content: "\e600";
+	}
+
+	.tui-slide-vcode {
+		background-color: #EAEEF1;
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-slide-glided {
+		width: 0;
+		height: 100%;
+		background-color: #19BE6B;
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 1;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-slider-block {
+		position: absolute;
+		z-index: 2;
+		background-color: #FFFFFF;
+		height: 100%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border: 1rpx solid;
+		box-sizing: border-box;
+		left: 0;
+		top: 0;
+		transition: border-color 0.08s;
+	}
+
+	.tui-text-flashover {
+		-webkit-background-clip: text !important;
+		-webkit-text-fill-color: transparent !important;
+		-webkit-animation: animate 1.8s infinite;
+	}
+
+	@-webkit-keyframes animate {
+		from {
+			background-position: -90rpx;
+		}
+
+		to {
+			background-position: 90rpx;
+		}
+	}
+
+	@keyframes animate {
+		from {
+			background-position: -90rpx;
+		}
+
+		to {
+			background-position: 90rpx;
+		}
+	}
+</style>

+ 73 - 0
components/thorui/tui-slide-verify/tui-slide-verify.wxs

@@ -0,0 +1,73 @@
+var slideBarWidth = 200;
+var slideBlockWidth = 32;
+var errorRange = 2
+var disabled = false
+
+function bool(str) {
+	return str === 'true' || str == true ? true : false
+}
+
+function touchstart(e, ins) {
+	var state=e.instance.getState()
+	var touch = e.touches[0] || e.changedTouches[0]
+	var dataset = e.instance.getDataset()
+	state.startX = touch.pageX
+	slideBarWidth = +dataset.slidebarwidth
+	slideBlockWidth = +dataset.slideblockwidth
+	errorRange = +dataset.errorrange
+	disabled = bool(dataset.disabled)
+}
+
+function styleChange(left, ins) {
+	if (!ins) return;
+	ins.selectComponent('.tui-slider-block').setStyle({
+		transform: 'translate3d(' + left + 'px,0,0)'
+	})
+	ins.selectComponent('.tui-slide-glided').setStyle({
+		width: left + 'px'
+	})
+}
+
+function touchmove(e, ins) {
+	if (disabled) return;
+	var state=e.instance.getState()
+	var touch = e.touches[0] || e.changedTouches[0]
+	var pageX = touch.pageX;
+	var left = pageX - state.startX + (state.lastLeft || 0);
+	left = left < 0 ? 0 : left;
+	var width = slideBarWidth - slideBlockWidth;
+	left = left >= width ? width : left;
+	state.startX = pageX
+	state.lastLeft = left
+	styleChange(left, ins)
+}
+
+function touchend(e, ins) {
+	if (disabled) return;
+	var state=e.instance.getState()
+	let left = slideBarWidth - slideBlockWidth
+	if (left - state.lastLeft <= errorRange) {
+		styleChange(left, ins)
+		ins.callMethod('success')
+	} else {
+		state.startX = 0;
+		state.lastLeft = 0;
+		styleChange(0, ins)
+	}
+}
+
+function slidereset(reset, oldreset, owner, ins) {
+	var state=ins.getState()
+	if (reset > 0) {
+		state.startX = 0;
+		state.lastLeft = 0;
+		styleChange(0, owner)
+	}
+}
+
+module.exports = {
+	touchstart: touchstart,
+	touchmove: touchmove,
+	touchend: touchend,
+	slidereset: slidereset
+}

+ 254 - 0
components/thorui/tui-steps/tui-steps.vue

@@ -0,0 +1,254 @@
+<template>
+	<view class="tui-steps-box" :class="{ 'tui-steps-column': direction === 'column' }">
+		<view
+			class="tui-step-item"
+			:style="{ width: direction === 'column' ? '100%' : spacing }"
+			:class="[direction === 'row' ? 'tui-step-horizontal' : 'tui-step-vertical']"
+			v-for="(item, index) in items"
+			:key="index"
+			@tap.stop="handleClick(index)"
+		>
+			<view class="tui-step-item-ico" :style="{ width: direction === 'column' ? '36rpx' : '100%' }">
+				<view
+					v-if="!item.name && !item.icon"
+					class="tui-step-ico"
+					:class="[direction === 'column' ? 'tui-step-column_ico' : 'tui-step-row_ico']"
+					:style="{
+						width: type == 2 || activeSteps === index ? '36rpx' : '16rpx',
+						height: type == 2 || activeSteps === index ? '36rpx' : '16rpx',
+						backgroundColor: index <= activeSteps ? activeColor : type == 2 ? '#fff' : deactiveColor,
+						borderColor: index <= activeSteps ? activeColor : deactiveColor
+					}"
+				>
+					<text v-if="activeSteps !== index" :style="{ color: index <= activeSteps ? '#fff' : '' }">{{ type == 1 ? '' : index + 1 }}</text>
+					<tui-icon name="check" :size="16" color="#fff" v-if="activeSteps === index"></tui-icon>
+				</view>
+				<view class="tui-step-custom" :style="{ backgroundColor: backgroundColor }" v-if="item.name || item.icon">
+					<tui-icon :name="item.name" :size="20" :color="index <= activeSteps ? activeColor : deactiveColor" v-if="item.name"></tui-icon>
+					<image :src="index <= activeSteps ? item.activeIcon : item.icon" class="tui-step-img" mode="widthFix" v-if="!item.name"></image>
+				</view>
+				<view
+					class="tui-step-line"
+					:class="['tui-step-' + direction + '_line', direction == 'column' && (item.name || item.icon) ? 'tui-custom-left' : '']"
+					:style="{
+						borderColor: index <= activeSteps - 1 ? activeColor : deactiveColor,
+						borderRightStyle: direction == 'column' ? lineStyle : '',
+						borderTopStyle: direction == 'column' ? '' : lineStyle
+					}"
+					v-if="index != items.length - 1"
+				></view>
+			</view>
+			<view class="tui-step-item-main" :class="['tui-step-' + direction + '_item_main']">
+				<view
+					class="tui-step-item-title"
+					:style="{
+						color: index <= activeSteps ? activeColor : deactiveColor,
+						fontSize: titleSize + 'rpx',
+						lineHeight: titleSize + 'rpx',
+						fontWeight: bold ? 'bold' : 'normal'
+					}"
+				>
+					{{ item.title }}
+				</view>
+				<view class="tui-step-item-content" :style="{ color: index <= activeSteps ? activeColor : deactiveColor, fontSize: descSize + 'rpx' }">{{ item.desc }}</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+//如果自定义传入图标内容,则需要拆分组件
+export default {
+	name: 'tuiSteps',
+	props: {
+		// 1-默认步骤 2-数字步骤
+		type: {
+			type: Number,
+			default: 1
+		},
+		spacing: {
+			type: String,
+			default: '160rpx'
+		},
+		// 方向 row column
+		direction: {
+			type: String,
+			default: 'row'
+		},
+		// 激活状态成功颜色
+		activeColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		// 未激活状态颜色
+		deactiveColor: {
+			type: String,
+			default: '#999999'
+		},
+		//title字体大小
+		titleSize: {
+			type: Number,
+			default: 28
+		},
+		//title是否粗体
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		//desc字体大小
+		descSize: {
+			type: Number,
+			default: 24
+		},
+		// 当前步骤
+		activeSteps: {
+			type: Number,
+			default: -1
+		},
+		//线条样式 同border线条样式
+		lineStyle: {
+			type: String,
+			default: 'solid'
+		},
+		/**
+			 * [{
+					title: "标题",
+					desc: "描述",
+					name:"字体图标 thorui icon内", 
+					icon:"图片图标", 
+					activeIcon:"已完成步骤显示图片图标"
+				}]
+			 * */
+		items: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		//自定义item内容时背景色
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		handleClick(index) {
+			this.$emit('click', { index: index });
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-steps-box {
+	width: 100%;
+	display: flex;
+	justify-content: center;
+}
+
+.tui-steps-column {
+	flex-direction: column;
+}
+
+.tui-step-ico {
+	border-radius: 80rpx;
+	position: relative;
+	z-index: 3;
+	margin: 0 auto;
+	border-width: 1rpx;
+	border-style: solid;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-step-row_ico {
+	top: 50%;
+	transform: translateY(-50%);
+}
+
+.tui-step-column_ico {
+	top: 0;
+}
+
+.tui-step-line {
+	position: absolute;
+	left: 50%;
+	top: 20rpx;
+	width: 100%;
+	height: 0rpx;
+	border-top-width: 1rpx;
+	z-index: 2;
+}
+
+.tui-step-row_item_main {
+	text-align: center;
+}
+
+.tui-step-item {
+	font-size: 24rpx;
+	position: relative;
+	box-sizing: border-box;
+}
+
+.tui-step-item-ico {
+	height: 36rpx;
+	line-height: 36rpx;
+	text-align: center;
+}
+.tui-step-custom {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 48rpx;
+	height: 40rpx;
+	position: relative;
+	z-index: 4;
+	margin: 0 auto;
+}
+.tui-step-img {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.tui-step-item-main {
+	margin-top: 16rpx;
+	clear: both;
+}
+
+.tui-step-item-title {
+	word-break: break-all;
+}
+
+.tui-step-item-content {
+	margin-top: 8rpx;
+	word-break: break-all;
+}
+
+.tui-step-vertical {
+	width: 100%;
+	display: flex;
+	padding-bottom: 60rpx;
+}
+
+.tui-step-column_item_main {
+	margin-top: 0;
+	padding-left: 20rpx;
+}
+
+.tui-step-column_line {
+	position: absolute;
+	height: 100%;
+	top: 0;
+	left: 18rpx;
+	margin: 0;
+	width: 0rpx;
+	border-right-width: 1rpx;
+}
+.tui-custom-left {
+	left: 20rpx !important;
+}
+</style>

+ 124 - 0
components/thorui/tui-sticky-wxs/tui-sticky-wxs.vue

@@ -0,0 +1,124 @@
+<template>
+	<view class="tui-sticky-class" :change:prop="parse.stickyChange" :prop="scrollTop" :data-top="top" :data-height="height"
+	 :data-stickytop="stickyTop" :data-container="container" :data-isNativeHeader="isNativeHeader" :data-index="index">
+		<!--sticky 容器-->
+		<view class="tui-sticky-seat" :style="{ height: stickyHeight, backgroundColor: backgroundColor }"></view>
+		<view class="tui-sticky-bar">
+			<slot name="header"></slot>
+		</view>
+		<!--sticky 容器-->
+		<!--内容-->
+		<slot name="content"></slot>
+	</view>
+</template>
+<script src="./tui-sticky.wxs" module="parse" lang="wxs"></script>
+<script>
+	export default {
+		name: 'tuiStickyWxs',
+		props: {
+			scrollTop: {
+				type: [Number, String],
+				value: 0
+			},
+			//吸顶时与顶部的距离,单位px
+			stickyTop: {
+				type: [Number, String],
+				// #ifndef H5
+				default: 0,
+				// #endif
+				// #ifdef H5
+				default: 44
+				// #endif
+			},
+			//是否指定容器,即内容放置插槽content内
+			container: {
+				type: Boolean,
+				default: false
+			},
+			//是否是原生自带header
+			isNativeHeader: {
+				type: Boolean,
+				default: true
+			},
+			//吸顶容器 高度 rpx
+			stickyHeight: {
+				type: String,
+				default: 'auto'
+			},
+			//占位容器背景颜色
+			backgroundColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//是否重新计算[有异步加载时使用,设置大于0的数]
+			recalc: {
+				type: Number,
+				default: 0
+			},
+			//列表中的索引值
+			index: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		watch: {
+			recalc(newValue, oldValue) {
+				this.updateScrollChange(() => {
+					//更新prop scrollTop值(+0.1即可),触发change事件
+					this.$emit("prop",{})
+				});
+			}
+		},
+		mounted() {
+			setTimeout(() => {
+				this.updateScrollChange();
+			}, 20);
+		},
+		data() {
+			return {
+				timer: null,
+				top: 0,
+				height: 0
+			};
+		},
+		methods: {
+			updateScrollChange(callback) {
+				if (this.timer) {
+					clearTimeout(this.timer);
+					this.timer = null;
+				}
+				this.timer = setTimeout(() => {
+					const className = '.tui-sticky-class';
+					const query = uni.createSelectorQuery().in(this);
+					query
+						.select(className)
+						.boundingClientRect(res => {
+							if (res) {
+								this.top = res.top + (this.scrollTop || 0);
+								this.height = res.height;
+								callback && callback();
+								this.$emit('change', {
+									index: Number(this.index),
+									top: this.top
+								});
+							}
+						})
+						.exec();
+				}, 0);
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-sticky-fixed {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		z-index: 998;
+	}
+
+	.tui-sticky-seat {
+		display: none;
+	}
+</style>

+ 44 - 0
components/thorui/tui-sticky-wxs/tui-sticky.wxs

@@ -0,0 +1,44 @@
+var stickyChange = function(scrollTop, oldScrollTop, ownerInstance, ins) {
+	if (!oldScrollTop && scrollTop === 0) return false;
+	var dataset = ins.getDataset()
+	var top = +dataset.top;
+	var height = +dataset.height;
+	var stickyTop = +dataset.stickytop;
+	var isNativeHeader = dataset.isnativeheader;
+	var isFixed = false;
+	var distance = stickyTop
+	// #ifdef H5
+	if (isNativeHeader) {
+		distance = distance - 44
+		distance = distance < 0 ? 0 : distance
+	}
+	// #endif
+	if (dataset.container) {
+		isFixed = (scrollTop + distance >= top && scrollTop + distance < top + height) ? true : false
+	} else {
+		isFixed = scrollTop + distance >= top ? true : false
+	}
+	if (isFixed) {
+		ownerInstance.selectComponent('.tui-sticky-bar').setStyle({
+			"top": stickyTop + 'px'
+		}).addClass('tui-sticky-fixed')
+		ownerInstance.selectComponent('.tui-sticky-seat').setStyle({
+			"display": 'block'
+		})
+	} else {
+		ownerInstance.selectComponent('.tui-sticky-bar').setStyle({
+			"top": 'auto'
+		}).removeClass('tui-sticky-fixed')
+		ownerInstance.selectComponent('.tui-sticky-seat').setStyle({
+			"display": 'none'
+		})
+	}
+	ownerInstance.triggerEvent("sticky", {
+		isFixed: isFixed,
+		index: parseInt(dataset.index)
+	})
+}
+
+module.exports = {
+	stickyChange: stickyChange
+}

+ 152 - 0
components/thorui/tui-sticky/tui-sticky.vue

@@ -0,0 +1,152 @@
+<template>
+	<view class="tui-sticky-class">
+		<!--sticky 容器-->
+		<view :style="{height: stickyHeight,backgroundColor:backgroundColor}" v-if="isFixed"></view>
+		<view :class="{'tui-sticky-fixed':isFixed}" :style="{top:isFixed?stickyTop+'px':'auto'}">
+			<slot name="header"></slot>
+		</view>
+		<!--sticky 容器-->
+		<!--内容-->
+		<slot name="content"></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiSticky",
+		props: {
+			scrollTop: {
+				type: Number
+			},
+			//吸顶时与顶部的距离,单位px
+			stickyTop: {
+				type: [Number, String]
+				// #ifndef H5
+				,default: 0
+				// #endif
+				// #ifdef H5
+				,default: 44
+				// #endif
+			},
+			//是否指定容器,即内容放置插槽content内
+			container: {
+				type: Boolean,
+				default: false
+			},
+			//是否是原生自带header
+			isNativeHeader: {
+				type: Boolean,
+				default: true
+			},
+			//吸顶容器 高度 rpx
+			stickyHeight: {
+				type: String,
+				default: "auto"
+			},
+			//占位容器背景颜色
+			backgroundColor: {
+				type: String,
+				default: "transparent"
+			},
+			//是否重新计算[有异步加载时使用,设置大于0的数]
+			recalc: {
+				type: Number,
+				default: 0
+			},
+			//列表中的索引值
+			index: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		watch: {
+			scrollTop(newValue, oldValue) {
+				if (this.initialize != 0) {
+					this.updateScrollChange(() => {
+						this.updateStickyChange();
+						this.initialize = 0
+					});
+				} else {
+					this.updateStickyChange();
+				}
+			},
+			recalc(newValue, oldValue) {
+				this.updateScrollChange(() => {
+					this.updateStickyChange();
+					this.initialize = 0;
+				});
+			}
+		},
+		created() {
+			this.initialize = this.recalc
+		},
+		mounted() {
+			setTimeout(() => {
+				this.updateScrollChange();
+			}, 20)
+		},
+		data() {
+			return {
+				timer: null,
+				top: 0,
+				height: 0,
+				isFixed: false,
+				initialize: 0 //重新初始化
+			};
+		},
+		methods: {
+			updateStickyChange() {
+				const top = this.top;
+				const height = this.height;
+				const scrollTop = this.scrollTop
+				let stickyTop = this.stickyTop
+				// #ifdef H5
+				if (this.isNativeHeader) {
+					stickyTop = stickyTop - 44
+					stickyTop = stickyTop < 0 ? 0 : stickyTop
+				}
+				// #endif
+				if (this.container) {
+					this.isFixed = (scrollTop + stickyTop >= top && scrollTop + stickyTop < top + height) ? true : false
+				} else {
+					this.isFixed = scrollTop + stickyTop >= top ? true : false
+				}
+				//是否吸顶
+				this.$emit("sticky", {
+					isFixed: this.isFixed,
+					index: this.index
+				})
+			},
+			updateScrollChange(callback) {
+				if (this.timer) {
+					clearTimeout(this.timer)
+					this.timer = null
+				}
+				this.timer = setTimeout(() => {
+					const className = '.tui-sticky-class';
+					const query = uni.createSelectorQuery().in(this);
+					query.select(className).boundingClientRect((res) => {
+						if (res) {
+							this.top = res.top + (this.scrollTop || 0);
+							this.height = res.height;
+							callback && callback();
+							this.$emit("change", {
+								index: Number(this.index),
+								top: this.top
+							})
+						}
+					}).exec()
+				}, 0)
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-sticky-fixed {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		z-index: 888;
+	}
+</style>

+ 310 - 0
components/thorui/tui-swipe-action/tui-swipe-action.vue

@@ -0,0 +1,310 @@
+<template>
+	<view class="tui-swipeout-wrap" :style="{ backgroundColor: backgroundColor }">
+		<view
+			class="tui-swipeout-item"
+			:class="[isShowBtn ? 'swipe-action-show' : '']"
+			@touchstart="handlerTouchstart"
+			@touchmove="handlerTouchmove"
+			@touchend="handlerTouchend"
+			@mousedown="handlerTouchstart"
+			@mousemove="handlerTouchmove"
+			@mouseup="handlerTouchend"
+			:style="{ transform: 'translate(' + position.pageX + 'px,0)' }"
+		>
+			<view class="tui-swipeout-content"><slot name="content"></slot></view>
+			<view class="tui-swipeout-button-right-group" v-if="actions.length > 0" @touchend.stop="loop">
+				<view
+					class="tui-swipeout-button-right-item"
+					v-for="(item, index) in actions"
+					:key="index"
+					:style="{ backgroundColor: item.background || '#f7f7f7', color: item.color, width: item.width + 'px' }"
+					:data-index="index"
+					@tap="handlerButton"
+				>
+					<image :src="item.icon" v-if="item.icon" :style="{ width: px(item.imgWidth), height: px(item.imgHeight) }"></image>
+					<text :style="{ fontSize: px(item.fontsize) }">{{ item.name }}</text>
+				</view>
+			</view>
+			<!--actions长度设置为0,可直接传按钮进来-->
+			<view
+				class="tui-swipeout-button-right-group"
+				@touchend.stop="loop"
+				@tap="handlerParentButton"
+				v-if="actions.length === 0"
+				:style="{ width: operateWidth + 'px', right: '-' + operateWidth + 'px' }"
+			>
+				<slot name="button"></slot>
+			</view>
+		</view>
+		<view v-if="isShowBtn && showMask" class="swipe-action_mask" @tap.stop="closeButtonGroup" @touchstart.stop.prevent="closeButtonGroup" />
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiSwipeAction',
+	props: {
+		// name: '删除',
+		// color: '#fff',
+		// fontsize: 32,//单位rpx
+		// width: 80, //单位px
+		// icon: 'like.png',//此处为图片地址
+		// background: '#ed3f14'
+		actions: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		//点击按钮时是否自动关闭
+		closable: {
+			type: Boolean,
+			default: true
+		},
+		//设为false,可以滑动多行不关闭菜单
+		showMask: {
+			type: Boolean,
+			default: true
+		},
+		operateWidth: {
+			type: Number,
+			default: 80
+		},
+		params: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		//禁止滑动
+		forbid: {
+			type: Boolean,
+			default: false
+		},
+		//手动开关
+		open: {
+			type: Boolean,
+			default: false
+		},
+		//背景色
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		}
+	},
+	watch: {
+		actions(newValue, oldValue) {
+			this.updateButtonSize();
+		},
+		open(newValue) {
+			this.manualSwitch(newValue);
+		}
+	},
+	data() {
+		return {
+			//start position
+			tStart: {
+				pageX: 0,
+				pageY: 0
+			},
+			//限制滑动距离
+			limitMove: 0,
+			//move position
+			position: {
+				pageX: 0,
+				pageY: 0
+			},
+			isShowBtn: false,
+			move: false
+		};
+	},
+	mounted() {
+		this.updateButtonSize();
+	},
+	methods: {
+		swipeDirection(x1, x2, y1, y2) {
+			return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : y1 - y2 > 0 ? 'Up' : 'Down';
+		},
+		//阻止事件冒泡
+		loop() {},
+		updateButtonSize() {
+			const actions = this.actions;
+			if (actions.length > 0) {
+				const query = uni.createSelectorQuery().in(this);
+				let limitMovePosition = 0;
+				actions.forEach(item => {
+					limitMovePosition += item.width || 0;
+				});
+				this.limitMove = limitMovePosition;
+			} else {
+				this.limitMove = this.operateWidth;
+			}
+		},
+		handlerTouchstart(event) {
+			if (this.forbid) return;
+			this.move = true;
+			let touches = event.touches ? event.touches[0] : {};
+			if (!touches || (touches.pageX === undefined && touches.pageY === undefined)) {
+				touches = { pageX: event.pageX, pageY: event.pageY };
+			}
+			const tStart = this.tStart;
+			if (touches) {
+				for (let i in tStart) {
+					if (touches[i]) {
+						tStart[i] = touches[i];
+					}
+				}
+			}
+		},
+		swipper(touches) {
+			const start = this.tStart;
+			const spacing = {
+				pageX: touches.pageX - start.pageX,
+				pageY: touches.pageY - start.pageY
+			};
+			if (this.limitMove < Math.abs(spacing.pageX)) {
+				spacing.pageX = -this.limitMove;
+			}
+			this.position = spacing;
+		},
+		handlerTouchmove(event) {
+			if (this.forbid || !this.move) return;
+			const start = this.tStart;
+			let touches = event.touches ? event.touches[0] : {};
+			if (!touches || (touches.pageX === undefined && touches.pageY === undefined)) {
+				touches = { pageX: event.pageX, pageY: event.pageY };
+			}
+			if (touches) {
+				const direction = this.swipeDirection(start.pageX, touches.pageX, start.pageY, touches.pageY);
+				if (direction === 'Left' && Math.abs(this.position.pageX) !== this.limitMove) {
+					this.swipper(touches);
+				}
+			}
+		},
+		handlerTouchend(event) {
+			if (this.forbid || !this.move) return;
+			this.move = false;
+			const start = this.tStart;
+			let touches = event.changedTouches ? event.changedTouches[0] : {};
+			if (!touches || (touches.pageX === undefined && touches.pageY === undefined)) {
+				touches = { pageX: event.pageX, pageY: event.pageY };
+			}
+			if (touches) {
+				const direction = this.swipeDirection(start.pageX, touches.pageX, start.pageY, touches.pageY);
+				const spacing = {
+					pageX: touches.pageX - start.pageX,
+					pageY: touches.pageY - start.pageY
+				};
+				if (Math.abs(spacing.pageX) >= 40 && direction === 'Left') {
+					spacing.pageX = spacing.pageX < 0 ? -this.limitMove : this.limitMove;
+					this.isShowBtn = true;
+				} else {
+					spacing.pageX = 0;
+				}
+				this.position = spacing;
+			}
+		},
+		handlerButton(event) {
+			if (this.closable) {
+				this.closeButtonGroup();
+			}
+			const dataset = event.currentTarget.dataset;
+			this.$emit('click', {
+				index: Number(dataset.index),
+				item: this.params
+			});
+		},
+		closeButtonGroup() {
+			this.position = {
+				pageX: 0,
+				pageY: 0
+			};
+			this.isShowBtn = false;
+		},
+		//控制自定义按钮菜单
+		handlerParentButton(event) {
+			if (this.closable) {
+				this.closeButtonGroup();
+			}
+		},
+		manualSwitch(isOpen) {
+			let x = 0;
+			if (isOpen) {
+				if (this.actions.length === 0) {
+					x = this.operateWidth;
+				} else {
+					let width = 0;
+					this.actions.forEach(item => {
+						width += item.width;
+					});
+					x = width;
+				}
+			}
+			this.position = {
+				pageX: -x,
+				pageY: 0
+			};
+		},
+		px(num) {
+			return uni.upx2px(num) + 'px';
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-swipeout-wrap {
+	position: relative;
+	overflow: hidden;
+}
+
+.swipe-action-show {
+	position: relative;
+	z-index: 998;
+}
+
+.tui-swipeout-item {
+	width: 100%;
+	/* padding: 15px 20px; */
+	box-sizing: border-box;
+	transition: transform 0.2s ease;
+	font-size: 14px;
+	cursor: pointer;
+}
+
+.tui-swipeout-content {
+	white-space: nowrap;
+	overflow: hidden;
+}
+
+.tui-swipeout-button-right-group {
+	position: absolute;
+	right: -100%;
+	top: 0;
+	height: 100%;
+	z-index: 1;
+	width: 100%;
+}
+
+.tui-swipeout-button-right-item {
+	height: 100%;
+	float: left;
+	white-space: nowrap;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	text-align: center;
+}
+
+.swipe-action_mask {
+	display: block;
+	opacity: 0;
+	position: fixed;
+	z-index: 997;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+</style>

+ 273 - 0
components/thorui/tui-tab/tui-tab.vue

@@ -0,0 +1,273 @@
+<template>
+	<scroll-view class="tui-scroll__view" :class="[isFixed && !isSticky?'tui-tabs__fixed':'',isSticky?'tui-tabs__sticky':'']"
+	 :style="{height: height+'rpx',background:backgroundColor,top: isFixed || isSticky ? top + 'px' : 'auto',zIndex:isFixed || isSticky?zIndex:'auto'}"
+	 :scroll-x="scrolling" :scroll-with-animation="scrolling" :scroll-left="scrollLeft">
+		<view class="tui-tabs__wrap">
+			<view class="tui-tabs__list" :class="[scroll ? 'tui-tabs__scroll' : '']" :style="{height: height+'rpx'}">
+				<view class="tui-tabs__item" :style="{height: height+'rpx'}" v-for="(item,index) in tabs" :key="index" @tap="handleClick"
+				 :data-index="index">
+					<view class="tui-item__child" :style="{	color: currentTab == index ? selectedColor : color,fontSize: size + 'rpx',lineHeight: size + 'rpx',fontWeight: bold && currentTab == index ? 'bold' : 'normal'}">{{item}}</view>
+				</view>
+				<view class="tui-tabs__line" :class="[needTransition ? 'tui-transition' : '']" :style="{background: sliderBgColor,height:sliderHeight,borderRadius: sliderRadius,bottom: bottom,width: lineWidth+'px',transform: `translateX(${translateX}px)`}"></view>
+			</view>
+		</view>
+	</scroll-view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiTab',
+		options: {
+			virtualHost: true
+		},
+		props: {
+			// 标签页数据源
+			tabs: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			// 当前选项卡
+			current: {
+				type: Number,
+				default: 0
+			},
+			// 是否可以滚动
+			scroll: {
+				type: Boolean,
+				default: false
+			},
+			// tab高度 rpx
+			height: {
+				type: Number,
+				default: 80
+			},
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			//字体大小
+			size: {
+				type: Number,
+				default: 28
+			},
+			//字体颜色
+			color: {
+				type: String,
+				default: '#666'
+			},
+			//选中后字体颜色
+			selectedColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//选中后 是否加粗 ,未选中则无效
+			bold: {
+				type: Boolean,
+				default: false
+			},
+			//滑块高度
+			sliderHeight: {
+				type: String,
+				default: '2px'
+			},
+			//滑块背景颜色
+			sliderBgColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//滑块 radius
+			sliderRadius: {
+				type: String,
+				default: '2px'
+			},
+			//滑块bottom
+			bottom: {
+				type: String,
+				default: '0'
+			},
+			//是否固定
+			isFixed: {
+				type: Boolean,
+				default: false
+			},
+			//吸顶效果,为true时isFixed失效
+			isSticky: {
+				type: Boolean,
+				default: false
+			},
+			//isFixed=true时,tab top值 px
+			top: {
+				type: Number,
+				// #ifndef H5
+				default: 0,
+				// #endif
+				// #ifdef H5
+				default: 44
+				// #endif
+			},
+			zIndex: {
+				type: [Number, String],
+				default: 996
+			}
+		},
+		watch: {
+			/**
+			 * 监听数据变化, 如果改变重新初始化参数
+			 */
+			tabs(newVal, oldVal) {
+				this.scrolling = false
+				// 异步加载数据时候, 延迟执行 init 方法, 防止无法正确获取 dom 信息
+				setTimeout(() => this.init(), 0);
+			},
+			/**
+			 *  监听 currentTab 变化, 做对应处理
+			 */
+			current(newVal, oldVal) {
+				this.scrollByIndex(newVal);
+			}
+		},
+		created() {
+			this.currentTab = this.current;
+		},
+		mounted() {
+			this.init()
+		},
+		data() {
+			return {
+				/* 未渲染数据 */
+				windowWidth: 0, // 屏幕宽度
+				tabItems: [], // 所有 tab 节点信息
+
+				/* 渲染数据 */
+				scrolling: true, // 控制 scroll-view 滚动以在异步加载数据的时候能正确获得 dom 信息
+				needTransition: false, // 下划线是否需要过渡动画
+				translateX: 0, // 下划 line 的左边距离
+				lineWidth: 0, // 下划 line 宽度
+				scrollLeft: 0, // scroll-view 左边滚动距离
+				currentTab: 0
+			};
+		},
+		methods: {
+			/**
+			 * 切换菜单
+			 */
+			handleClick(e) {
+				let index = Number(e.currentTarget.dataset.index)
+				this.$emit('change', {
+					index: index
+				});
+				this.scrollByIndex(index);
+			},
+			/**
+			 * 滑动到指定位置
+			 * @param tabCur: 当前激活的tabItem的索引
+			 * @param needTransition: 下划线是否需要过渡动画, 第一次进来应设置为false
+			 */
+			scrollByIndex(tabCur, needTransition = true) {
+				let item = this.tabItems[tabCur];
+				if (!item) return;
+				let itemWidth = item.width || 0,
+					itemLeft = item.left || 0;
+				this.needTransition = needTransition;
+				this.currentTab = tabCur;
+				// 超出滚动的情况
+				if (this.scroll) {
+					// 保持滚动后当前 item '尽可能' 在屏幕中间
+					let scrollLeft = itemLeft - (this.windowWidth - itemWidth) / 2;
+					this.scrollLeft = scrollLeft;
+					this.translateX = itemLeft;
+					this.lineWidth = itemWidth
+				} else {
+					// 不超出滚动的情况
+					this.translateX = itemLeft;
+					this.lineWidth = itemWidth
+				}
+			},
+			/**
+			 *  初始化函数
+			 */
+			init() {
+				const {
+					windowWidth
+				} = uni.getSystemInfoSync();
+				this.windowWidth = windowWidth || 375
+				const query = uni.createSelectorQuery().in(this);
+				query.selectAll(".tui-item__child").boundingClientRect((res) => {
+					this.scrolling = true;
+					this.tabItems = res;
+					this.scrollByIndex(this.currentTab, false);
+				}).exec();
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-scroll__view {
+		width: 100%;
+		height: 80rpx;
+		overflow: hidden;
+	}
+
+	.tui-tabs__fixed {
+		position: fixed;
+		left: 0;
+	}
+
+	.tui-tabs__sticky {
+		position: sticky;
+		left: 0;
+	}
+
+	.tui-tabs__wrap {
+		padding-bottom: 20rpx;
+	}
+
+	.tui-tabs__list {
+		position: relative;
+		height: 80rpx;
+		display: flex;
+	}
+
+	.tui-tabs__scroll {
+		white-space: nowrap !important;
+		display: block !important;
+	}
+
+	.tui-tabs__scroll .tui-tabs__item {
+		padding: 0 30rpx;
+		display: inline-flex;
+	}
+
+	.tui-tabs__scroll .tui-item__child {
+		display: block !important;
+	}
+
+	.tui-tabs__item {
+		flex: 1;
+		text-align: center;
+		padding: 0 10rpx;
+		box-sizing: border-box;
+		transition: color 0.3s ease-in-out;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-item__child {
+		display: inline-block;
+	}
+
+	.tui-tabs__line {
+		position: absolute;
+		left: 0;
+		width: 0;
+		display: inline-block;
+	}
+
+	.tui-tabs__line.tui-transition {
+		transition: width 0.3s, transform 0.3s;
+	}
+</style>

+ 306 - 0
components/thorui/tui-tabbar/tui-tabbar.vue

@@ -0,0 +1,306 @@
+<template>
+	<view class="tui-tabbar"
+		:class="{ 'tui-tabbar-fixed': isFixed, 'tui-unlined': unlined, 'tui-backdrop__filter': backdropFilter }"
+		:style="{ background: backgroundColor, zIndex: isFixed ? zIndex : 'auto' }">
+		<block v-for="(item, index) in tabBar" :key="index">
+			<view class="tui-tabbar-item" :class="{ 'tui-item-hump': item.hump }"
+				:style="{ backgroundColor: item.hump && !backdropFilter ? backgroundColor : 'none' }"
+				@tap="tabbarSwitch(index, item)">
+				<view class="tui-icon-box" :class="{ 'tui-tabbar-hump': item.hump }">
+					<image :src="current == item.pagePath ? item.selectedIconPath : item.iconPath"
+						:class="[item.hump ? '' : 'tui-tabbar-icon']"></image>
+					<view :class="[item.isDot ? 'tui-badge-dot' : 'tui-badge']"
+						:style="{ color: badgeColor, backgroundColor: badgeBgColor }" v-if="item.num">
+						{{ item.isDot ? '' : item.num }}
+					</view>
+				</view>
+				<view class="tui-text-scale" :class="{ 'tui-text-hump': item.hump }"
+					:style="{ color: current == item.pagePath ? selectedColor : color }">{{ item.text }}</view>
+			</view>
+		</block>
+		<view :style="{ background: backgroundColor }" :class="{ 'tui-hump-box': hump }"
+			v-if="hump && !unlined && !backdropFilter"></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiTabbar',
+	props: {
+		//当前索引
+		current: {
+			type: String,
+			default: ''
+		},
+		//当前索引
+		mo: {
+			type: String,
+			default: ''
+		},
+		pageid: {
+			type: String,
+			default: '-1'
+		},
+		//是否需要中间凸起按钮
+		hump: {
+			type: Boolean,
+			default: false
+		},
+		//固定在底部
+		isFixed: {
+			type: Boolean,
+			default: true
+		},
+		//tabbar
+		// "pagePath": "/pagesA/my/my", 页面路径
+		// "text": "thor", 标题
+		// "iconPath": "thor_gray.png", 图标地址
+		// "selectedIconPath": "thor_active.png", 选中图标地址
+		// "hump": true, 是否为凸起图标
+		// "num": 2,   角标数量
+		// "isDot": true,  角标是否为圆点
+		// "verify": true  是否验证  (如登录)
+
+		//角标字体颜色
+		badgeColor: {
+			type: String,
+			default: '#fff'
+		},
+		//角标背景颜色
+		badgeBgColor: {
+			type: String,
+			default: '#F74D54'
+		},
+		unlined: {
+			type: Boolean,
+			default: false
+		},
+		//是否开启高斯模糊效果[仅在支持的浏览器有效果]
+		backdropFilter: {
+			type: Boolean,
+			default: false
+		},
+		//z-index
+		zIndex: {
+			type: [Number, String],
+			default: 99
+		}
+	},
+	data() {
+		return {
+			tabBar: [],
+			//字体颜色
+			color: '#666',
+			//字体选中颜色
+			selectedColor: '#5677FC',
+			//背景颜色
+			backgroundColor: '#FFFFFF',
+		};
+	},
+	watch: {
+		current() { },
+		mo() { }
+	},
+	mounted() {
+		let _this = this
+		_this.$request.post('bottommenu.list', {
+			mo: _this.mo,
+			pageid: _this.pageid
+		}).then(res => {
+			if (res.errno == 0) {
+				_this.color = res.data.color;
+				_this.selectedColor = res.data.selectedColor;
+				_this.backgroundColor = res.data.backgroundColor;
+				_this.tabBar = res.data.list;
+			}
+		})
+	},
+
+	methods: {
+		tabbarSwitch(index, link) {
+			var pagePath = link.pagePath;
+			if (link.ptype == 'customurl') {
+				if (link.zdyLinktype == 'wxapp') {
+					uni.navigateToMiniProgram({
+						appId: link.zdyappid,
+						path: pagePath
+					})
+				} else if (link.zdyLinktype == 'web') {
+					this.sam.navigateTo("/pages/webview/h5?url=" + pagePath);
+				} else {
+					this.sam.navigateTo(pagePath);
+				}
+			} else {
+				if (pagePath.indexOf("?") != -1) {
+					pagePath = pagePath + '&from=bottom'
+				} else {
+					pagePath = pagePath + '?from=bottom'
+				}
+				this.sam.navigateTo(pagePath);
+			}
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-tabbar {
+	width: 100%;
+	height: 100rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	position: relative;
+}
+
+.tui-backdrop__filter {
+	/* Safari for macOS & iOS */
+	-webkit-backdrop-filter: blur(15px);
+	/* Google Chrome */
+	backdrop-filter: blur(15px);
+}
+
+.tui-tabbar-fixed {
+	position: fixed;
+	left: 0;
+	bottom: 0;
+	padding-bottom: constant(safe-area-inset-bottom);
+	padding-bottom: env(safe-area-inset-bottom);
+	box-sizing: content-box !important;
+}
+
+.tui-tabbar::before {
+	content: ' ';
+	width: 100%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 0;
+	display: block;
+	z-index: 3;
+}
+
+.tui-tabbar-item {
+	height: 100%;
+	flex: 1;
+	display: flex;
+	text-align: center;
+	align-items: center;
+	flex-direction: column;
+	justify-content: space-between;
+	position: relative;
+	padding: 10rpx 0;
+	box-sizing: border-box;
+	z-index: 5;
+}
+
+.tui-icon-box {
+	position: relative;
+}
+
+.tui-item-hump {
+	height: 98rpx;
+}
+
+.tui-tabbar-icon {
+	width: 52rpx;
+	height: 52rpx;
+	display: block;
+}
+
+.tui-hump-box {
+	width: 120rpx;
+	height: 120rpx;
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+	top: -50rpx;
+	border-radius: 50%;
+	z-index: 4;
+}
+
+.tui-hump-box::after {
+	content: ' ';
+	height: 200%;
+	width: 200%;
+	border: 1px solid #b2b2b2;
+	position: absolute;
+	top: 0;
+	left: 0;
+	transform: scale(0.5) translateZ(0);
+	transform-origin: 0 0;
+	border-radius: 120rpx;
+	box-sizing: border-box;
+	display: block;
+}
+
+.tui-unlined::after {
+	height: 0 !important;
+}
+
+.tui-tabbar-hump {
+	width: 100rpx;
+	height: 100rpx;
+	position: absolute;
+	left: 50%;
+	-webkit-transform: translateX(-50%) rotate(0deg);
+	transform: translateX(-50%) rotate(0deg);
+	top: -40rpx;
+	-webkit-transition: all 0.2s linear;
+	transition: all 0.2s linear;
+	border-radius: 50%;
+	z-index: 5;
+}
+
+.tui-tabbar-hump image {
+	width: 100rpx;
+	height: 100rpx;
+	display: block;
+}
+
+.tui-hump-active {
+	-webkit-transform: translateX(-50%) rotate(135deg);
+	transform: translateX(-50%) rotate(135deg);
+}
+
+.tui-text-scale {
+	font-weight: bold;
+	transform: scale(0.8);
+	font-size: 25rpx;
+	line-height: 28rpx;
+	transform-origin: center 100%;
+}
+
+.tui-text-hump {
+	position: absolute;
+	left: 50%;
+	bottom: 10rpx;
+	transform: scale(0.8) translateX(-50%);
+	transform-origin: 0 100%;
+}
+
+.tui-badge {
+	position: absolute;
+	font-size: 24rpx;
+	height: 32rpx;
+	min-width: 20rpx;
+	padding: 0 6rpx;
+	border-radius: 40rpx;
+	right: 0;
+	top: -5rpx;
+	transform: translateX(70%);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-badge-dot {
+	position: absolute;
+	height: 16rpx;
+	width: 16rpx;
+	border-radius: 50%;
+	right: -4rpx;
+	top: -4rpx;
+}
+</style>

+ 303 - 0
components/thorui/tui-tabs/tui-tabs.vue

@@ -0,0 +1,303 @@
+<template>
+	<view
+		class="tui-tabs-view"
+		:class="[isFixed ? 'tui-tabs-fixed' : 'tui-tabs-relative', unlined ? 'tui-unlined' : '']"
+		:style="{
+			width: tabsWidth + 'px',
+			height: height + 'rpx',
+			padding: `0 ${padding}rpx`,
+			background: backgroundColor,
+			top: isFixed ? top + 'px' : 'auto',
+			zIndex: isFixed ? zIndex : 'auto'
+		}"
+	>
+		<view v-for="(item, index) in tabs" :key="index" class="tui-tabs-item" :style="{ width: itemWidth }" @tap.stop="swichTabs(index)">
+			<view
+				class="tui-tabs-title"
+				:class="{ 'tui-tabs-active': currentTab == index, 'tui-tabs-disabled': item.disabled }"
+				:style="{
+					color: currentTab == index ? selectedColor : color,
+					fontSize: size + 'rpx',
+					lineHeight: size + 'rpx',
+					fontWeight: bold && currentTab == index ? 'bold' : 'normal'
+				}"
+			>
+			<view :class="[item.isDot ? 'tui-badge-dot' : 'tui-badge']" :style="{ color: badgeColor, backgroundColor: badgeBgColor }" v-if="item.num">
+				{{ item.isDot ? '' : item.num }}
+			</view>
+				{{ item.name }}
+			</view>
+		</view>
+		<view
+			class="tui-tabs-slider"
+			:style="{
+				transform: 'translateX(' + scrollLeft + 'px)',
+				width: sliderWidth + 'rpx',
+				height: sliderHeight + 'rpx',
+				borderRadius: sliderRadius,
+				bottom: bottom,
+				background: sliderBgColor,
+				marginBottom: bottom == '50%' ? '-' + sliderHeight / 2 + 'rpx' : 0
+			}"
+		></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiTabs',
+	props: {
+		//标签页
+		tabs: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		//tabs宽度,不传值则默认使用windowWidth,单位px
+		width: {
+			type: Number,
+			default: 0
+		},
+		//rpx
+		height: {
+			type: Number,
+			default: 80
+		},
+		//rpx 只对左右padding起作用,上下为0
+		padding: {
+			type: Number,
+			default: 30
+		},
+		//背景色
+		backgroundColor: {
+			type: String,
+			default: '#FFFFFF'
+		},
+		//是否固定
+		isFixed: {
+			type: Boolean,
+			default: false
+		},
+		//px
+		top: {
+			type: Number,
+			// #ifndef H5
+			default: 0,
+			// #endif
+			// #ifdef H5
+			default: 44
+			// #endif
+		},
+		//是否去掉底部线条
+		unlined: {
+			type: Boolean,
+			default: false
+		},
+		//当前选项卡
+		currentTab: {
+			type: Number,
+			default: 0
+		},
+		//滑块宽度
+		sliderWidth: {
+			type: Number,
+			default: 68
+		},
+		//滑块高度
+		sliderHeight: {
+			type: Number,
+			default: 6
+		},
+		//滑块背景颜色
+		sliderBgColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		sliderRadius: {
+			type: String,
+			default: '50rpx'
+		},
+		//滑块bottom
+		bottom: {
+			type: String,
+			default: '0'
+		},
+		//标签页宽度
+		itemWidth: {
+			type: String,
+			default: '25%'
+		},
+		//字体颜色
+		color: {
+			type: String,
+			default: '#666'
+		},
+		//选中后字体颜色
+		selectedColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//字体大小
+		size: {
+			type: Number,
+			default: 28
+		},
+		//选中后 是否加粗 ,未选中则无效
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		zIndex: {
+			type: [Number, String],
+			default: 996
+		}
+	},
+	watch: {
+		currentTab() {
+			this.checkCor();
+		},
+		tabs() {
+			this.checkCor();
+		},
+		width(val) {
+			this.tabsWidth = val;
+			this.checkCor();
+		}
+	},
+	created() {
+		// 高度自适应
+		setTimeout(() => {
+			uni.getSystemInfo({
+				success: res => {
+					this.winWidth = res.windowWidth;
+					this.tabsWidth = this.width == 0 ? this.winWidth : this.width;
+					this.checkCor();
+				}
+			});
+		}, 0);
+	},
+	data() {
+		return {
+			winWidth: 0,
+			tabsWidth: 0,
+			scrollLeft: 0
+		};
+	},
+	methods: {
+		checkCor: function() {
+			let tabsNum = this.tabs.length;
+			let padding = (this.winWidth / 750) * this.padding;
+			let width = this.tabsWidth - padding * 2;
+			let left = (width / tabsNum - (this.winWidth / 750) * this.sliderWidth) / 2 + padding;
+			let scrollLeft = left;
+			if (this.currentTab > 0) {
+				scrollLeft = scrollLeft + (width / tabsNum) * this.currentTab;
+			}
+			this.scrollLeft = scrollLeft;
+		},
+		// 点击标题切换当前页时改变样式
+		swichTabs: function(index) {
+			let item = this.tabs[index];
+			if (item && item.disabled) return;
+			if (this.currentTab == index) {
+				return false;
+			} else {
+				this.$emit('change', {
+					index: Number(index)
+				});
+			}
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-tabs-view {
+	width: 100%;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.tui-tabs-relative {
+	position: relative;
+}
+
+.tui-tabs-fixed {
+	position: fixed;
+	left: 0;
+}
+
+.tui-tabs-fixed::before,
+.tui-tabs-relative::before {
+	content: '';
+	position: absolute;
+	border-bottom: 1rpx solid #eaeef1;
+	-webkit-transform: scaleY(0.5) translateZ(0);
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 100%;
+	bottom: 0;
+	right: 0;
+	left: 0;
+}
+
+.tui-unlined::before {
+	border-bottom: 0 !important;
+}
+
+.tui-tabs-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-tabs-disabled {
+	opacity: 0.6;
+}
+
+.tui-tabs-title {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	position: relative;
+	z-index: 2;
+}
+
+.tui-tabs-active {
+	transition: all 0.15s ease-in-out;
+}
+
+.tui-tabs-slider {
+	position: absolute;
+	left: 0;
+	transition: all 0.15s ease-in-out;
+	z-index: 0;
+	transform: translateZ(0);
+}
+.tui-badge {
+	position: absolute;
+	font-size: 24rpx;
+	color: #fff;
+	height: 32rpx;
+	min-width: 20rpx;
+	padding: 0 6rpx;
+	border-radius: 50%;
+	background-color: #F34B0B;
+	right: -10rpx;
+	top: -10rpx;
+	transform: translateX(70%);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.tui-badge-dot {
+	position: absolute;
+	height: 16rpx;
+	width: 16rpx;
+	border-radius: 50%;
+	right: -4rpx;
+	top: -4rpx;
+}
+</style>

+ 354 - 0
components/thorui/tui-tag/tui-tag.vue

@@ -0,0 +1,354 @@
+<template>
+	<view class="tui-tag" :hover-class="hover ? 'tui-tag-opcity' : ''" :hover-stay-time="150" :class="[originLeft ? 'tui-origin-left' : '', originRight ? 'tui-origin-right' : '', getClassName(shape, plain), getTypeClass(type, plain)]"
+	 :style="{ transform: `scale(${scaleMultiple})`, padding: padding, margin: margin, fontSize: size, lineHeight: size }"
+	 @tap="handleClick">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiTag',
+		props: {
+			type: {
+				type: String,
+				default: 'primary'
+			},
+			//padding
+			padding: {
+				type: String,
+				default: '16rpx 26rpx'
+			},
+			margin: {
+				type: String,
+				default: '0'
+			},
+			//文字大小 rpx
+			size: {
+				type: String,
+				default: '28rpx'
+			},
+			// circle, square,circleLeft,circleRight
+			shape: {
+				type: String,
+				default: 'square'
+			},
+			//是否空心
+			plain: {
+				type: Boolean,
+				default: false
+			},
+			//点击效果
+			hover: {
+				type: Boolean,
+				default: false
+			},
+			//缩放倍数
+			scaleMultiple: {
+				type: Number,
+				default: 1
+			},
+			originLeft: {
+				type: Boolean,
+				default: false
+			},
+			originRight: {
+				type: Boolean,
+				default: false
+			},
+			index: {
+				type: Number,
+				default: 0
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {
+					index: this.index
+				});
+			},
+			getTypeClass: function(type, plain) {
+				return plain ? 'tui-' + type + '-outline' : 'tui-' + type;
+			},
+			getClassName: function(shape, plain) {
+				//circle, square,circleLeft,circleRight
+				var className = plain ? 'tui-tag-outline ' : '';
+				if (shape != 'square') {
+					if (shape == 'circle') {
+						className = className + (plain ? 'tui-tag-outline-fillet' : 'tui-tag-fillet');
+					} else if (shape == 'circleLeft') {
+						className = className + 'tui-tag-fillet-left';
+					} else if (shape == 'circleRight') {
+						className = className + 'tui-tag-fillet-right';
+					}
+				}
+				return className;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	/* color start*/
+
+	.tui-primary {
+		background-color: #5677fc !important;
+		color: #fff;
+	}
+
+	.tui-light-primary {
+		background-color: #5c8dff !important;
+		color: #fff;
+	}
+
+	.tui-dark-primary {
+		background-color: #4a67d6 !important;
+		color: #fff;
+	}
+
+	.tui-dLight-primary {
+		background-color: #4e77d9 !important;
+		color: #fff;
+	}
+
+	.tui-danger {
+		background-color: #ed3f14 !important;
+		color: #fff;
+	}
+
+	.tui-red {
+		background-color: #ff201f !important;
+		color: #fff;
+	}
+
+	.tui-warning {
+		background-color: #ff7900 !important;
+		color: #fff;
+	}
+
+	.tui-green {
+		background-color: #19be6b !important;
+		color: #fff;
+	}
+
+	.tui-high-green {
+		background-color: #52dcae !important;
+		color: #52dcae;
+	}
+
+	.tui-black {
+		background-color: #000 !important;
+		color: #fff;
+	}
+
+	.tui-white {
+		background-color: #fff !important;
+		color: #333 !important;
+	}
+
+	.tui-translucent {
+		background-color: rgba(0, 0, 0, 0.7);
+	}
+
+	.tui-light-black {
+		background-color: #333 !important;
+	}
+
+	.tui-gray {
+		background-color: #ededed !important;
+	}
+
+	.tui-phcolor-gray {
+		background-color: #ccc !important;
+	}
+
+	.tui-divider-gray {
+		background-color: #eaeef1 !important;
+	}
+
+	.tui-btn-gray {
+		background-color: #ededed !important;
+		color: #999 !important;
+	}
+
+	.tui-hover-gray {
+		background-color: #f7f7f9 !important;
+	}
+
+	.tui-bg-gray {
+		background-color: #fafafa !important;
+	}
+
+	.tui-light-blue {
+		background-color: #ecf6fd;
+		color: #4dabeb !important;
+	}
+
+	.tui-light-brownish {
+		background-color: #fcebef;
+		color: #8a5966 !important;
+	}
+
+	.tui-light-orange {
+		background-color: #fef5eb;
+		color: #faa851 !important;
+	}
+
+	.tui-light-green {
+		background-color: #e8f6e8;
+		color: #44cf85 !important;
+	}
+
+	.tui-primary-outline::after {
+		border: 1px solid #5677fc !important;
+	}
+
+	.tui-primary-outline {
+		color: #5677fc !important;
+		background-color: none;
+	}
+
+	.tui-danger-outline {
+		color: #ed3f14 !important;
+		background-color: none;
+	}
+
+	.tui-danger-outline::after {
+		border: 1px solid #ed3f14 !important;
+	}
+
+	.tui-red-outline {
+		color: #ff201f !important;
+		background-color: none;
+	}
+
+	.tui-red-outline::after {
+		border: 1px solid #ff201f !important;
+	}
+
+	.tui-warning-outline {
+		color: #ff7900 !important;
+		background-color: none;
+	}
+
+	.tui-warning-outline::after {
+		border: 1px solid #ff7900 !important;
+	}
+
+	.tui-green-outline {
+		color: #44cf85 !important;
+		background-color: none;
+	}
+
+	.tui-green-outline::after {
+		border: 1px solid #44cf85 !important;
+	}
+
+	.tui-high-green-outline {
+		color: #52dcae !important;
+		background-color: none;
+	}
+
+	.tui-high-green-outline::after {
+		border: 1px solid #52dcae !important;
+	}
+
+	.tui-gray-outline {
+		color: #999 !important;
+		background-color: none;
+	}
+
+	.tui-gray-outline::after {
+		border: 1px solid #ccc !important;
+	}
+
+	.tui-black-outline {
+		color: #333 !important;
+		background-color: none;
+	}
+
+	.tui-black-outline::after {
+		border: 1px solid #333 !important;
+	}
+
+	.tui-white-outline {
+		color: #fff !important;
+		background-color: none;
+	}
+
+	.tui-white-outline::after {
+		border: 1px solid #fff !important;
+	}
+
+	/* color end*/
+
+	/* tag start*/
+
+	.tui-tag {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border-radius: 6rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-tag-outline {
+		position: relative;
+		background-color: none;
+		color: #5677fc;
+	}
+
+	.tui-tag-outline::after {
+		content: ' ';
+		position: absolute;
+		width: 200%;
+		height: 200%;
+		transform: scale(0.5) translateZ(0);
+		transform-origin: 0 0;
+		box-sizing: border-box;
+		left: 0;
+		top: 0;
+		border-radius: 12rpx;
+	}
+
+	.tui-tag-fillet {
+		border-radius: 50rpx;
+	}
+
+	.tui-white.tui-tag-fillet::after {
+		border-radius: 80rpx;
+	}
+
+	.tui-tag-outline-fillet::after {
+		border-radius: 80rpx;
+	}
+
+	.tui-tag-fillet-left {
+		border-radius: 50rpx 0 0 50rpx;
+	}
+
+	.tui-tag-fillet-right {
+		border-radius: 0 50rpx 50rpx 0;
+	}
+
+	.tui-tag-fillet-left.tui-tag-outline::after {
+		border-radius: 100rpx 0 0 100rpx;
+	}
+
+	.tui-tag-fillet-right.tui-tag-outline::after {
+		border-radius: 0 100rpx 100rpx 0;
+	}
+
+	/* tag end*/
+	.tui-origin-left {
+		transform-origin: 0 center;
+	}
+
+	.tui-origin-right {
+		transform-origin: 100% center;
+	}
+
+	.tui-tag-opcity {
+		opacity: 0.5;
+	}
+</style>

+ 38 - 0
components/thorui/tui-time-axis/tui-time-axis.vue

@@ -0,0 +1,38 @@
+<template>
+	<view class="tui-timeaxis-class tui-time-axis">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:"tuiTimeAxis",
+		data() {
+			return {
+
+			};
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-time-axis {
+		padding-left: 20px;
+		box-sizing: border-box;
+		position: relative;
+	}
+
+	.tui-time-axis::before {
+		content: " ";
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 1px;
+		bottom: 0;
+		border-left: 1px solid #ddd;
+		-webkit-transform-origin: 0 0;
+		transform-origin: 0 0;
+		-webkit-transform: scaleX(0.5);
+		transform: scaleX(0.5);
+	}
+</style>

+ 50 - 0
components/thorui/tui-timeaxis-item/tui-timeaxis-item.vue

@@ -0,0 +1,50 @@
+<template>
+	<view class="tui-timeaxis-item">
+		<slot name="content"></slot>
+		<view class="tui-timeaxis-node" :style="{backgroundColor:backgroundColor}">
+			<slot name="node"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiTimeaxisItem",
+		props: {
+			//节点背景色
+			backgroundColor: {
+				type: String,
+				default: "#fafafa"
+			}
+		},
+		data() {
+			return {
+
+			};
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-timeaxis-item {
+		position: relative;
+		width: 100%;
+		display: flex;
+		flex-direction: column;
+		margin-bottom: 25px;
+	}
+
+	.tui-timeaxis-node {
+		position: absolute;
+		top: 0;
+		left: -20px;
+		transform-origin: 0;
+		transform: translateX(-50%);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		z-index: 99;
+		background-color: #fafafa;
+		font-size: 24rpx;
+	}
+</style>

+ 329 - 0
components/thorui/tui-timer/tui-timer.vue

@@ -0,0 +1,329 @@
+<template>
+	<view class="tui-timer__box">
+		<view class="tui-timer__item" :style="{
+				background: backgroundColor,
+				border: borderWidth > 0 ? `${borderWidth}rpx solid ${borderColor}` : 0,
+				width: backgroundColor == 'transparent' && borderColor == 'transparent' ? 'auto' : getWidth(d, width) + 'rpx',
+				height: height + 'rpx',
+				fontSize: size + 'rpx',
+				color: color
+			}"
+		 v-if="isDays">
+			{{ d }}
+		</view>
+		<view class="tui-timer__colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+		 v-if="isDays">天</view>
+		<view class="tui-timer__item" :style="{
+				background: backgroundColor,
+				border: borderWidth > 0 ? `${borderWidth}rpx solid ${borderColor}` : 0,
+				width: getWidth(d, width) + 'rpx',
+				height: height + 'rpx',
+				fontSize: size + 'rpx',
+				color: color
+			}"
+		 v-if="isHours">
+			{{ h }}
+		</view>
+		<view class="tui-timer__colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+		 v-if="isHours">{{ isColon ? ':' : '时' }}</view>
+		<view class="tui-timer__item" :style="{
+				background: backgroundColor,
+				border: borderWidth > 0 ? `${borderWidth}rpx solid ${borderColor}` : 0,
+				width: getWidth(d, width) + 'rpx',
+				height: height + 'rpx',
+				fontSize: size + 'rpx',
+				color: color
+			}"
+		 v-if="isMinutes">
+			{{ m }}
+		</view>
+		<view class="tui-timer__colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+		 v-if="isMinutes">{{ isColon ? ':' : '分' }}</view>
+		<view class="tui-timer__item" :style="{
+				background: backgroundColor,
+				border: borderWidth > 0 ? `${borderWidth}rpx solid ${borderColor}` : 0,
+				width: getWidth(d, width) + 'rpx',
+				height: height + 'rpx',
+				fontSize: size + 'rpx',
+				color: color
+			}"
+		 v-if="isSeconds">
+			{{ s }}
+		</view>
+		<view class="tui-timer__colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+		 v-if="isSeconds">{{ isColon ? '' : '秒' }}</view>
+		<view class="tui-timer__colon" :style="{ lineHeight: colonSize + 'rpx', fontSize: colonSize + 'rpx', color: colonColor }"
+		 v-if="isSeconds && isMs">.</view>
+		<view class="tui-timer__ms" :style="{
+				background: backgroundColor,
+				border: borderWidth > 0 ? `${borderWidth}rpx solid ${borderColor}` : 0,
+				fontSize: msSize + 'rpx',
+				color: msColor,
+				height: height + 'rpx',
+				width: msWidth > 0 ? msWidth + 'rpx' : 'auto'
+			}"
+		 v-if="isSeconds && isMs">
+			<view :class="{ 'tui-ms__list': ani }">
+				<view class="tui-ms__item" :style="{ height: height + 'rpx' }" v-for="(item, index) in ms" :key="index">{{ item }}</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiTimer',
+		emits: ['end'],
+		props: {
+			//数字框宽度
+			width: {
+				type: Number,
+				default: 36
+			},
+			//数字框高度
+			height: {
+				type: Number,
+				default: 36
+			},
+			borderWidth: {
+				type: Number,
+				default: 0
+			},
+			//数字框border颜色
+			borderColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//数字框背景颜色
+			backgroundColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//数字框字体大小
+			size: {
+				type: Number,
+				default: 32
+			},
+			//数字框字体颜色
+			color: {
+				type: String,
+				default: '#333'
+			},
+			//冒号或文字大小
+			colonSize: {
+				type: Number,
+				default: 32
+			},
+			//冒号或文字颜色
+			colonColor: {
+				type: String,
+				default: '#333'
+			},
+			//初始时间,单位s
+			value: {
+				type: [Number,String],
+				default: 0
+			},
+			//最大时间,大于等于最大时间则计时结束,为0则需要手动结束 (单位:秒)
+			maxTime: {
+				type: Number,
+				default: 0
+			},
+			//是否显示天
+			isDays: {
+				type: Boolean,
+				default: false
+			},
+			//是否显示小时
+			isHours: {
+				type: Boolean,
+				default: true
+			},
+			//是否显示分钟
+			isMinutes: {
+				type: Boolean,
+				default: true
+			},
+			//是否显示秒数
+			isSeconds: {
+				type: Boolean,
+				default: true
+			},
+			//是否显示毫秒
+			isMs: {
+				type: Boolean,
+				default: false
+			},
+			msWidth: {
+				type: Number,
+				default: 0
+			},
+			msSize: {
+				type: Number,
+				default: 28
+			},
+			msColor: {
+				type: String,
+				default: '#333'
+			},
+			//时分秒是否展示为冒号,false为文字
+			isColon: {
+				type: Boolean,
+				default: true
+			},
+			//是否自动开始(传值false,则需要手动调用方法)
+			start: {
+				type: Boolean,
+				default: true
+			}
+		},
+		data() {
+			return {
+				d: '0',
+				h: '00',
+				m: '00',
+				s: '00',
+				ms: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+				seconds: 0,
+				loop: null,
+				ani: false
+			};
+		},
+		created() {
+			this.seconds = Number(this.value);
+			this.timer(this.seconds);
+			if (this.start) {
+				this.startTiming();
+			}
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.clearTimer();
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.clearTimer();
+		},
+		// #endif
+		watch: {
+			value(val) {
+				this.clearTimer();
+				this.seconds = Number(val);
+				this.timer(this.seconds);
+				setTimeout(() => {
+					if (this.start) {
+						this.startTiming();
+					}
+				}, 0);
+			}
+		},
+		methods: {
+			getWidth(num, width) {
+				return num > 99 ? (width / 2) * num.toString().length : width;
+			},
+			clearTimer() {
+				this.ani = false;
+				clearInterval(this.loop);
+				this.loop = null;
+			},
+			//开始
+			startTiming() {
+				if (this.seconds >= this.maxTime && this.maxTime != 0) {
+					this.endTimer();
+					return
+				}
+				this.clearTimer();
+				this.ani = true;
+				this.loop = setInterval(() => {
+					this.seconds++;
+					this.timer(this.seconds);
+					if (this.seconds >= this.maxTime && this.maxTime != 0) {
+						this.endTimer();
+					}
+				}, 1000);
+			},
+			//重置
+			resetTimer() {
+				this.d = '0';
+				this.h = '00';
+				this.m = '00';
+				this.s = '00';
+				this.seconds = 0;
+				this.clearTimer();
+				setTimeout(() => {
+					this.startTiming();
+				}, 0);
+			},
+			//结束 | 暂停
+			endTimer() {
+				this.clearTimer();
+				this.$emit('end', {
+					day: this.d,
+					hour: this.h,
+					minute: this.m,
+					second: this.s,
+					totalSeconds: this.seconds
+				});
+			},
+			timer(seconds) {
+				let [day, hour, minute, second] = [0, 0, 0, 0];
+				if (seconds > 0) {
+					day = this.isDays ? Math.floor(seconds / (60 * 60 * 24)) : 0;
+					hour = this.isHours ? Math.floor(seconds / (60 * 60)) - day * 24 : 0;
+					minute = this.isMinutes ? Math.floor(seconds / 60) - hour * 60 - day * 24 * 60 : 0;
+					second = this.isSeconds ? Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60 : 0;
+				}
+				hour = hour < 10 ? '0' + hour : hour;
+				minute = minute < 10 ? '0' + minute : minute;
+				second = second < 10 ? '0' + second : second;
+				this.d = day;
+				this.h = hour;
+				this.m = minute;
+				this.s = second;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-timer__box {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-timer__item,
+	.tui-timer__colon {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border-radius: 6rpx;
+	}
+
+	.tui-timer__ms {
+		overflow: hidden;
+		border-radius: 6rpx;
+	}
+
+	/*ms使用css3代替js频繁更新操作,性能优化*/
+	.tui-ms__list {
+		animation: loop 1s steps(10) infinite;
+	}
+
+	@keyframes loop {
+		from {
+			transform: translateY(0);
+		}
+
+		to {
+			transform: translateY(-100%);
+		}
+	}
+
+	.tui-ms__item {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+</style>

+ 129 - 0
components/thorui/tui-tips/tui-tips.vue

@@ -0,0 +1,129 @@
+<template>
+	<block v-if="position == 'top'">
+		<view class="tui-tips-class tui-toptips" :style="{backgroundColor:backgroundColor,color:color,fontSize:size+'rpx'}" :class="[show ? 'tui-top-show' : '']">{{ msg }}</view>
+	</block>
+	<block v-else>
+		<view class="tui-tips-class tui-toast" :class="[position == 'center' ? 'tui-centertips' : 'tui-bottomtips', show ? 'tui-toast-show' : '']">
+			<view class="tui-tips-content" :style="{backgroundColor:backgroundColor,color:color,fontSize:size+'rpx'}">{{ msg }}</view>
+		</view>
+	</block>
+</template>
+
+<script>
+export default {
+	name: 'tuiTips',
+	props: {
+		//top bottom center
+		position: {
+			type: String,
+			default: 'top'
+		},
+		backgroundColor: {
+			type: String,
+			default: 'rgba(0, 0, 0, 0.7)'
+		},
+		color: {
+			type: String,
+			default: '#fff'
+		},
+		size: {
+			type: Number,
+			default: 30
+		}
+	},
+	data() {
+		return {
+			timer: null,
+			show: false,
+			msg: '无法连接到服务器~'
+		};
+	},
+	methods: {
+		showTips: function(options) {
+			const {duration = 2000 } = options;
+			clearTimeout(this.timer);
+			this.show = true;
+			// this.duration = duration < 2000 ? 2000 : duration;
+			this.msg = options.msg;
+			this.timer = setTimeout(() => {
+				this.show = false;
+				clearTimeout(this.timer);
+				this.timer = null;
+			}, duration);
+		}
+	}
+};
+</script>
+
+<style scoped>
+/*顶部消息提醒 start*/
+.tui-toptips {
+	width: 100%;
+	padding: 18rpx 30rpx;
+	box-sizing: border-box;
+	position: fixed;
+	z-index: 9999;
+	left: 0;
+	top: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	word-break: break-all;
+	opacity: 0;
+	transform: translateZ(0) translateY(-100%);
+	transition: all 0.3s ease-in-out;
+}
+
+.tui-top-show {
+	transform: translateZ(0) translateY(0);
+	opacity: 1;
+}
+
+/*顶部消息提醒 end*/
+
+/*toast消息提醒 start*/
+
+/*注意问题:
+ 1、fixed 元素宽度无法自适应,所以增加了子元素
+ 2、fixed 和 display冲突导致动画效果消失,暂时使用visibility替代
+*/
+.tui-toast {
+	width: 80%;
+	box-sizing: border-box;
+	color: #fff;
+	font-size: 28rpx;
+	position: fixed;
+	visibility: hidden;
+	opacity: 0;
+	left: 50%;
+	transition: all 0.3s ease-in-out;
+	z-index: 9999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-toast-show {
+	visibility: visible;
+	opacity: 1;
+}
+
+.tui-tips-content {
+	word-wrap: break-word;
+	word-break: break-all;
+	border-radius: 8rpx;
+	padding: 18rpx 30rpx;
+}
+
+.tui-bottomtips {
+	bottom: 120rpx;
+	-webkit-transform: translateX(-50%);
+	transform: translateX(-50%);
+}
+
+.tui-centertips {
+	top: 50%;
+	-webkit-transform: translate(-50%, -50%);
+	transform: translate(-50%, -50%);
+}
+</style>

+ 121 - 0
components/thorui/tui-toast/tui-toast.vue

@@ -0,0 +1,121 @@
+<template>
+	<view class="tui-toast" :class="[visible?'tui-toast-show':'',content?'tui-toast-padding':'',icon?'':'tui-unicon-padding']" :style="{width:getWidth(icon,content),zIndex:zIndex}">
+		<image :src="imgUrl" class="tui-toast-img" v-if="icon"></image>
+		<view class="tui-toast-text" :class="[icon?'':'tui-unicon']">{{title}}</view>
+		<view class="tui-toast-text tui-content-ptop" v-if="content && icon">{{content}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiToast",
+		props: {
+			zIndex:{
+				type:Number,
+				default:99999
+			}
+		},
+		data() {
+			return {
+				timer: null,
+				//是否显示
+				visible: false,
+				//显示标题
+				title: "操作成功",
+				//显示内容
+				content: "",
+				//是否有icon
+				icon:false,
+				imgUrl:""
+			};
+		},
+		methods: {
+			show: function(options) {
+				let {
+					duration = 2000,
+					icon=false
+				} = options;
+				clearTimeout(this.timer);
+				this.visible = true;
+				this.title = options.title || "";
+				this.content = options.content || "";
+				this.icon=icon;
+				if(icon && options.imgUrl){
+					this.imgUrl=options.imgUrl
+				}
+				this.timer = setTimeout(() => {
+					this.visible = false;
+					clearTimeout(this.timer);
+					this.timer = null;
+				}, duration);
+			},
+			getWidth(icon,content){
+				let width="auto";
+				if(icon){
+					width=content?'420rpx':'360rpx'
+				}
+				return width
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-toast {
+		background-color: rgba(0, 0, 0, 0.75);
+		border-radius: 10rpx;
+		position: fixed;
+		visibility: hidden;
+		opacity: 0;
+		left: 50%;
+		top: 48%;
+		-webkit-transform: translate(-50%, -50%);
+		transform: translate(-50%, -50%);
+		transition:  0.3s ease-in-out;
+		transition-property:opacity,visibility;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		padding: 60rpx 20rpx 54rpx 20rpx;
+		box-sizing: border-box;
+	}
+
+	.tui-toast-padding {
+		padding-top: 50rpx !important;
+		padding-bottom: 50rpx !important;
+	}
+	.tui-unicon-padding {
+		padding: 24rpx 40rpx !important;
+		word-break: break-all;
+	}
+
+	.tui-toast-show {
+		visibility: visible;
+		opacity: 1;
+	}
+
+
+	.tui-toast-img {
+		width: 120rpx;
+		height: 120rpx;
+		display: block;
+		margin-bottom: 28rpx;
+	}
+
+	.tui-toast-text {
+		font-size: 30rpx;
+		line-height: 30rpx;
+		font-weight: 400;
+		color: #fff;
+		text-align: center;
+	}
+	.tui-unicon{
+		line-height: 40rpx !important;
+		font-size: 32rpx !important;
+	}
+	.tui-content-ptop {
+		padding-top: 10rpx;
+		font-size: 26rpx !important;
+	}
+</style>

+ 104 - 0
components/thorui/tui-top-dropdown/tui-top-dropdown.vue

@@ -0,0 +1,104 @@
+<template>
+	<view>
+		<view
+			class="tui-top-dropdown tui-dropdown-box"
+			:class="[show ? 'tui-dropdown-show' : '']"
+			:style="{
+				height: height ? px(height) : 'auto',
+				backgroundColor: backgroundColor,
+				paddingBottom: px(paddingbtm),
+				transform: 'translateZ(0) translateY(' + (show ? px(translatey) : '-100%') + ')'
+			}"
+		>
+			<slot></slot>
+		</view>
+		<view @touchmove.stop.prevent class="tui-dropdown-mask" :class="[mask && show ? 'tui-mask-show' : '']" @tap="handleClose"></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiTopDropdown',
+	props: {
+		//是否需要mask
+		mask: {
+			type: Boolean,
+			default: true
+		},
+		//控制显示
+		show: {
+			type: Boolean,
+			default: false
+		},
+		//背景颜色
+		backgroundColor: {
+			type: String,
+			default: '#f2f2f2'
+		},
+		//padding-bottom  rpx
+		paddingbtm: {
+			type: Number,
+			default: 0
+		},
+		//高度 rpx
+		height: {
+			type: Number,
+			default: 580
+		},
+		//移动距离 需要计算
+		translatey: {
+			type: Number,
+			default: 0
+		}
+	},
+	methods: {
+		handleClose() {
+			if (!this.show) {
+				return;
+			}
+			this.$emit('close', {});
+		},
+		px(num) {
+			return uni.upx2px(num) + 'px';
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-dropdown-box {
+	width: 100%;
+	position: fixed;
+	box-sizing: border-box;
+	border-bottom-right-radius: 24rpx;
+	border-bottom-left-radius: 24rpx;
+	transform: translateZ(0);
+	overflow: hidden;
+	/* visibility: hidden; */
+	transition: all 0.3s ease-in-out;
+	z-index: 996;
+	top: 0;
+}
+
+.tui-dropdown-show {
+	/* visibility: visible; */
+}
+
+.tui-dropdown-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	z-index: 986;
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	visibility: hidden;
+}
+
+.tui-mask-show {
+	opacity: 1;
+	visibility: visible;
+}
+</style>

+ 373 - 0
components/thorui/tui-upload/tui-upload.vue

@@ -0,0 +1,373 @@
+<template>
+	<view class="tui-container">
+		<view class="tui-upload-box">
+			<view class="tui-image-item" v-for="(item,index) in imageList" :key="index">
+				<image :src="item" class="tui-item-img" @tap.stop="previewImage(index)" mode="aspectFill"></image>
+				<view v-if="!forbidDel" class="tui-img-del" @tap.stop="delImage(index)"></view>
+				<view v-if="statusArr[index]!=1" class="tui-upload-mask">
+					<view class="tui-upload-loading" v-if="statusArr[index]==2"></view>
+					<text class="tui-tips">{{statusArr[index]==2?'上传中...':'上传失败'}}</text>
+					<view class="tui-mask-btn" v-if="statusArr[index]==3" @tap.stop="reUpLoad(index)" hover-class="tui-btn-hover"
+					 :hover-stay-time="150">重新上传</view>
+				</view>
+			</view>
+			<view v-if="isShowAdd" class="tui-upload-add" @tap="chooseImage">
+				<view class="tui-upload-icon tui-icon-plus"></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiUpload',
+		props: {
+			//初始化图片路径
+			value: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//禁用删除
+			forbidDel: {
+				type: Boolean,
+				default: false
+			},
+			//禁用添加
+			forbidAdd: {
+				type: Boolean,
+				default: false
+			},
+			//限制数
+			limit: {
+				type: Number,
+				default: 9
+			},
+			//original 原图,compressed 压缩图,默认二者都有
+			sizeType: {
+				type: Array,
+				default () {
+					return ['original', 'compressed']
+				}
+			},
+			//album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
+			sourceType: {
+				type: Array,
+				default () {
+					return ['album', 'camera']
+				}
+			},
+			//可上传图片类型,默认为空,不限制  Array<String> ['jpg','png','gif']
+			imageFormat: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//单张图片大小限制 MB 
+			size: {
+				type: Number,
+				default: 4
+			},
+			//自定义参数
+			params: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		data() {
+			return {
+				//图片地址
+				imageList: [],
+				//上传状态:1-上传成功 2-上传中 3-上传失败
+				statusArr: []
+			}
+		},
+		created() {
+			this.initImages()
+		},
+		watch: {
+			value(val) {
+				if (val) {
+					this.initImages()
+				}
+			}
+		},
+		computed: {
+			isShowAdd() {
+				let isShow = true;
+				if (this.forbidAdd || (this.limit && this.imageList.length >= this.limit)) {
+					isShow = false;
+				}
+				return isShow
+			}
+		},
+		methods: {
+			initImages() {
+				this.imageList = [...this.value];
+				for (let item of this.imageList) {
+					this.statusArr.push("1")
+				}
+			},
+			// 重新上传
+			reUpLoad(index) {
+				this.$set(this.statusArr, index, "2")
+				this.change()
+				this.uploadImage(index, this.imageList[index]).then(() => {
+					this.change()
+				}).catch(() => {
+					this.change()
+				})
+			},
+			change() {
+				let status = ~this.statusArr.indexOf("2") ? 2 : 1
+				if (status != 2 && ~this.statusArr.indexOf("3")) {
+					// 上传失败
+					status = 3
+				}
+				this.$emit('complete', {
+					status: status,
+					imgArr: this.imageList,
+					params:this.params
+				})
+			},
+			toast(text) {
+				text && uni.showToast({
+					title: text,
+					icon: "none"
+				});
+			},
+			chooseImage: function() {
+				let _this = this;
+				uni.chooseImage({
+					count: _this.limit - _this.imageList.length,
+					sizeType: _this.sizeType,
+					sourceType: _this.sourceType,
+					success: function(e) {
+						let imageArr = [];
+						for (let i = 0; i < e.tempFiles.length; i++) {
+							let len = _this.imageList.length;
+							if (len >= _this.limit) {
+								_this.toast(`最多可上传${_this.limit}张图片`);
+								break;
+							}
+							//过滤图片类型
+							let path = e.tempFiles[i].path;
+
+							if (_this.imageFormat.length > 0) {
+								let format = ""
+								// #ifdef H5
+								let type = e.tempFiles[i].type;
+								format = type.split('/')[1]
+								// #endif
+
+								// #ifndef H5
+								format = path.split(".")[(path.split(".")).length - 1];
+								// #endif
+
+								if (_this.imageFormat.indexOf(format) == -1) {
+									let text = `只能上传 ${_this.imageFormat.join(',')} 格式图片!`
+									_this.toast(text);
+									continue;
+								}
+							}
+
+							//过滤超出大小限制图片
+							let size = e.tempFiles[i].size;
+
+							if (_this.size * 1024 * 1024 < size){
+								let err=`单张图片大小不能超过:${_this.size}MB`
+								_this.toast(err);
+								continue;
+							}
+							imageArr.push(path)
+							_this.imageList.push(path)
+							_this.statusArr.push("2")
+						}
+						_this.change()
+
+						let start = _this.imageList.length - imageArr.length
+						for (let j = 0; j < imageArr.length; j++) {
+							let index = start + j
+							_this.uploadImage(index, imageArr[j]).then(() => {
+								_this.change()
+							}).catch(() => {
+								_this.change()
+							})
+						}
+					}
+				})
+			},
+			uploadImage: function(index, url) {
+				let _this = this;
+				return new Promise((resolve, reject) => {
+					_this.$request.uploadFile(url).then(res => {
+						res.url && (_this.imageList[index] = res.url)
+						_this.$set(_this.statusArr, index, res.url ? "1" : "3")
+						resolve(index)
+					});
+				})
+				
+
+			},
+			delImage: function(index) {
+				this.imageList.splice(index, 1)
+				this.statusArr.splice(index, 1)
+				this.$emit("remove", {
+					index: index,
+					params:this.params
+				})
+				this.change()
+			},
+			previewImage: function(index) {
+				if (!this.imageList.length) return;
+				uni.previewImage({
+					current: this.imageList[index],
+					loop: true,
+					urls: this.imageList
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: 'tuiUpload';
+		src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAATcAA0AAAAAByQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEwAAAABoAAAAciR52BUdERUYAAASgAAAAHgAAAB4AKQALT1MvMgAAAaAAAABCAAAAVjxvR/tjbWFwAAAB+AAAAEUAAAFK5ibpuGdhc3AAAASYAAAACAAAAAj//wADZ2x5ZgAAAkwAAADXAAABAAmNjcZoZWFkAAABMAAAAC8AAAA2FpiS+WhoZWEAAAFgAAAAHQAAACQH3QOFaG10eAAAAeQAAAARAAAAEgwAACBsb2NhAAACQAAAAAwAAAAMAEoAgG1heHAAAAGAAAAAHwAAACABEgA2bmFtZQAAAyQAAAFJAAACiCnmEVVwb3N0AAAEcAAAACgAAAA6OMUs4HjaY2BkYGAAYo3boY/i+W2+MnCzMIDAzb3qdQj6fwPzf+YGIJeDgQkkCgA/KAtvAHjaY2BkYGBu+N/AEMPCAALM/xkYGVABCwBZ4wNrAAAAeNpjYGRgYGBl0GJgZgABJiDmAkIGhv9gPgMADTABSQB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PGJ9xMjf8b2CIYW5gaAAKM4LkANt9C+UAAHjaY2GAABYIVmBgAAAA+gAtAAAAeNpjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYnjE+4/z/n4EBQksxSf6GqgcCRjYGOIeRCUgwMaACRoZhDwCiLwmoAAAAAAAAAAAAAAAASgCAeNpdjkFKw0AARf/vkIR0BkPayWRKQZtYY90ohJju2kOIbtz0KD1HVm50UfEmWXoAr9ADOHFARHHzeY//Fx8Ci+FJfIgdJFa4AhgiMshbrCuIsLxhFJZVs+Vl1bT1GddtbXTC3OhohN4dg4BJ3zMJAnccyfm468ZzHXddrH9ZKbHzdf9n/vkY/xv9sPQXgGEvBrHHwst5kTbXLE+YpYVPkxepPmW94W16UbdNJd6f3SAzo5W7m1jaKd+8ZZIvk5nlKw9SK6Wle7BLS3f/bTzQLmfAF2T1NsQAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMGiTIxMjMxsKak5qSWpbFmZiRmJ+QAmgAUIAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABAABAAQAAAACAAAAAHjaY2BgYGQAgqtL1DlA9M296nUwGgA+8QYgAAA=) format('woff');
+		font-weight: normal;
+		font-style: normal;
+	}
+
+	.tui-upload-icon {
+		font-family: "tuiUpload" !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+		padding: 10rpx;
+	}
+
+	.tui-icon-delete:before {
+		content: "\e601";
+	}
+
+	.tui-icon-plus:before {
+		content: "\e609";
+	}
+
+	.tui-upload-box {
+		width: 100%;
+		display: flex;
+		flex-wrap: wrap;
+	}
+
+	.tui-upload-add {
+		width: 220rpx;
+		height: 220rpx;
+		font-size: 68rpx;
+		font-weight: 100;
+		color: #888;
+		background-color: #F7F7F7;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 0;
+	}
+
+	.tui-image-item {
+		width: 220rpx;
+		height: 220rpx;
+		position: relative;
+		margin-right: 20rpx;
+		margin-bottom: 20rpx;
+	}
+
+	.tui-image-item:nth-of-type(3n) {
+		margin-right: 0;
+	}
+
+	.tui-item-img {
+		width: 220rpx;
+		height: 220rpx;
+		display: block;
+	}
+
+	.tui-img-del {
+		width: 36rpx;
+		height: 36rpx;
+		position: absolute;
+		right: -12rpx;
+		top: -12rpx;
+		background-color: #EB0909;
+		border-radius: 50%;
+		color: white;
+		font-size: 34rpx;
+		z-index: 999;
+	}
+
+	.tui-img-del::before {
+		content: '';
+		width: 16rpx;
+		height: 1px;
+		position: absolute;
+		left: 10rpx;
+		top: 18rpx;
+		background-color: #fff;
+	}
+
+	.tui-upload-mask {
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		left: 0;
+		top: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: space-around;
+		padding: 40rpx 0;
+		box-sizing: border-box;
+		background-color: rgba(0, 0, 0, 0.6);
+	}
+
+	.tui-upload-loading {
+		width: 28rpx;
+		height: 28rpx;
+		border-radius: 50%;
+		border: 2px solid;
+		border-color: #B2B2B2 #B2B2B2 #B2B2B2 #fff;
+		animation: tui-rotate 0.7s linear infinite;
+	}
+
+	@keyframes tui-rotate {
+		0% {
+			transform: rotate(0);
+		}
+
+		100% {
+			transform: rotate(360deg);
+		}
+	}
+
+	.tui-tips {
+		font-size: 26rpx;
+		color: #fff;
+	}
+
+	.tui-mask-btn {
+		padding: 4rpx 16rpx;
+		border-radius: 40rpx;
+		text-align: center;
+		font-size: 24rpx;
+		color: #fff;
+		border: 1rpx solid #fff;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-btn-hover {
+		opacity: 0.8;
+	}
+</style>

+ 400 - 0
components/uni-notice-bar/uni-notice-bar.vue

@@ -0,0 +1,400 @@
+<template>
+	<view v-if="show" class="uni-noticebar" :style="{ backgroundColor: backgroundColor }" @click="onClick">
+		<!-- #ifdef MP-ALIPAY -->
+		<view v-if="showClose === true || showClose === 'true'" class="uni-noticebar-close uni-cursor-point" @click="close">
+			<uni-icons type="closeempty" :color="color" size="12" />
+		</view>
+		<view v-if="showIcon === true || showIcon === 'true'" class="uni-noticebar-icon">
+			<uni-icons type="sound" :color="color" size="14" />
+		</view>
+		<!-- #endif -->
+		<!-- #ifndef MP-ALIPAY -->
+		<uni-icons v-if="showClose === true || showClose === 'true'" class="uni-noticebar-close uni-cursor-point" type="closeempty" :color="color"
+		 size="12" @click="close" />
+		<uni-icons v-if="showIcon === true || showIcon === 'true'" class="uni-noticebar-icon" type="sound" :color="color"
+		 size="14" />
+		<!-- #endif -->
+		<view ref="textBox" class="uni-noticebar__content-wrapper" :class="{'uni-noticebar__content-wrapper--scrollable':scrollable, 'uni-noticebar__content-wrapper--single':!scrollable && (single || moreText)}">
+			<view :id="elIdBox" class="uni-noticebar__content" :class="{'uni-noticebar__content--scrollable':scrollable, 'uni-noticebar__content--single':!scrollable && (single || moreText)}">
+				<text :id="elId" ref="animationEle" class="uni-noticebar__content-text" :class="{'uni-noticebar__content-text--scrollable':scrollable,'uni-noticebar__content-text--single':!scrollable && (single || moreText)}"
+				 :style="{color:color, width:wrapWidth+'px',fontSize:fontsize +'px', 'animationDuration': animationDuration, '-webkit-animationDuration': animationDuration ,animationPlayState: webviewHide?'paused':animationPlayState,'-webkit-animationPlayState':webviewHide?'paused':animationPlayState, animationDelay: animationDelay, '-webkit-animationDelay':animationDelay}">{{text}}</text>
+			</view>
+		</view>
+		<view v-if="showGetMore === true || showGetMore === 'true'" class="uni-noticebar__more uni-cursor-point" @click="clickMore">
+			<text v-if="moreText" :style="{ color: moreColor }" class="uni-noticebar__more-text">{{ moreText }}</text>
+			<uni-icons type="arrowright" :color="moreColor" size="14" />
+		</view>
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const dom = weex.requireModule('dom');
+	const animation = weex.requireModule('animation');
+	// #endif
+
+	/**
+	 * NoticeBar 自定义导航栏
+	 * @description 通告栏组件
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=30
+	 * @property {Number} speed 文字滚动的速度,默认100px/秒
+	 * @property {String} text 显示文字
+	 * @property {String} backgroundColor 背景颜色
+	 * @property {String} color 文字颜色
+	 * @property {String} moreColor 查看更多文字的颜色
+	 * @property {String} moreText 设置“查看更多”的文本
+	 * @property {Boolean} single = [true|false] 是否单行
+	 * @property {Boolean} scrollable = [true|false] 是否滚动,为true时,NoticeBar为单行
+	 * @property {Boolean} showIcon = [true|false] 是否显示左侧喇叭图标
+	 * @property {Boolean} showClose = [true|false] 是否显示左侧关闭按钮
+	 * @property {Boolean} showGetMore = [true|false] 是否显示右侧查看更多图标,为true时,NoticeBar为单行
+	 * @event {Function} click 点击 NoticeBar 触发事件
+	 * @event {Function} close 关闭 NoticeBar 触发事件
+	 * @event {Function} getmore 点击”查看更多“时触发事件
+	 */
+
+	export default {
+		name: 'UniNoticeBar',
+		props: {
+			text: {
+				type: String,
+				default: ''
+			},
+			moreText: {
+				type: String,
+				default: ''
+			},
+			backgroundColor: {
+				type: String,
+				default: '#fffbe8'
+			},
+			speed: {
+				// 默认1s滚动100px
+				type: Number,
+				default: 100
+			},
+			fontsize: {
+				type: Number,
+				default: 14
+			},
+			color: {
+				type: String,
+				default: '#de8c17'
+			},
+			moreColor: {
+				type: String,
+				default: '#999999'
+			},
+			single: {
+				// 是否单行
+				type: [Boolean, String],
+				default: false
+			},
+			scrollable: {
+				// 是否滚动,添加后控制单行效果取消
+				type: [Boolean, String],
+				default: false
+			},
+			showIcon: {
+				// 是否显示左侧icon
+				type: [Boolean, String],
+				default: false
+			},
+			showGetMore: {
+				// 是否显示右侧查看更多
+				type: [Boolean, String],
+				default: false
+			},
+			showClose: {
+				// 是否显示左侧关闭按钮
+				type: [Boolean, String],
+				default: false
+			}
+		},
+		data() {
+			const elId = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			const elIdBox = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			return {
+				textWidth: 0,
+				boxWidth: 0,
+				wrapWidth: '',
+				webviewHide: false,
+				// #ifdef APP-NVUE
+				stopAnimation: false,
+				// #endif
+				elId: elId,
+				elIdBox: elIdBox,
+				show: true,
+				animationDuration: 'none',
+				animationPlayState: 'paused',
+				animationDelay: '0s'
+			}
+		},
+		mounted() {
+			// #ifdef APP-PLUS
+			var pages = getCurrentPages();
+			var page = pages[pages.length - 1];
+			var currentWebview = page.$getAppWebview();
+			currentWebview.addEventListener('hide',()=>{
+				this.webviewHide = true
+			})
+			currentWebview.addEventListener('show',()=>{
+				this.webviewHide = false
+			})
+			// #endif
+			this.$nextTick(() => {
+				this.initSize()
+			})
+		},
+		// #ifdef APP-NVUE
+		beforeDestroy() {
+			this.stopAnimation = true
+		},
+		// #endif
+		methods: {
+			initSize() {
+				if (this.scrollable) {
+					// #ifndef APP-NVUE
+					let query = [],
+						boxWidth = 0,
+						textWidth = 0;
+					let textQuery = new Promise((resolve, reject) => {
+						uni.createSelectorQuery()
+							// #ifndef MP-ALIPAY
+							.in(this)
+							// #endif
+							.select(`#${this.elId}`)
+							.boundingClientRect()
+							.exec(ret => {
+								this.textWidth = ret[0].width
+								resolve()
+							})
+					})
+					let boxQuery = new Promise((resolve, reject) => {
+						uni.createSelectorQuery()
+							// #ifndef MP-ALIPAY
+							.in(this)
+							// #endif
+							.select(`#${this.elIdBox}`)
+							.boundingClientRect()
+							.exec(ret => {
+								this.boxWidth = ret[0].width
+								resolve()
+							})
+					})
+					query.push(textQuery)
+					query.push(boxQuery)
+					Promise.all(query).then(() => {
+						this.animationDuration = `${this.textWidth / this.speed}s`
+						this.animationDelay = `-${this.boxWidth / this.speed}s`
+						setTimeout(() => {
+							this.animationPlayState = 'running'
+						}, 1000)
+					})
+					// #endif
+					// #ifdef APP-NVUE
+					dom.getComponentRect(this.$refs['animationEle'], (res) => {
+						let winWidth = uni.getSystemInfoSync().windowWidth
+						this.textWidth = res.size.width
+						animation.transition(this.$refs['animationEle'], {
+							styles: {
+								transform: `translateX(-${winWidth}px)`
+							},
+							duration: 0,
+							timingFunction: 'linear',
+							delay: 0
+						}, () => {
+							if (!this.stopAnimation) {
+								animation.transition(this.$refs['animationEle'], {
+									styles: {
+										transform: `translateX(-${this.textWidth}px)`
+									},
+									timingFunction: 'linear',
+									duration: (this.textWidth - winWidth) / this.speed * 1000,
+									delay: 1000
+								}, () => {
+									if (!this.stopAnimation) {
+										this.loopAnimation()
+									}
+								});
+							}
+						});
+					})
+					// #endif
+				}
+				// #ifdef APP-NVUE
+				if (!this.scrollable && (this.single || this.moreText)) {
+					dom.getComponentRect(this.$refs['textBox'], (res) => {
+						this.wrapWidth = res.size.width
+					})
+				}
+				// #endif
+			},
+			loopAnimation() {
+				// #ifdef APP-NVUE
+				animation.transition(this.$refs['animationEle'], {
+					styles: {
+						transform: `translateX(0px)`
+					},
+					duration: 0
+				}, () => {
+					if (!this.stopAnimation) {
+						animation.transition(this.$refs['animationEle'], {
+							styles: {
+								transform: `translateX(-${this.textWidth}px)`
+							},
+							duration: this.textWidth / this.speed * 1000,
+							timingFunction: 'linear',
+							delay: 0
+						}, () => {
+							if (!this.stopAnimation) {
+								this.loopAnimation()
+							}
+						});
+					}
+				});
+				// #endif
+			},
+			clickMore() {
+				this.$emit('getmore')
+			},
+			close() {
+				this.show = false;
+				this.$emit('close')
+			},
+			onClick() {
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.uni-noticebar {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		padding: 6px 12px;
+	}
+
+	.uni-cursor-point {
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-noticebar-close {
+		margin-right: 5px;
+	}
+
+	.uni-noticebar-icon {
+		margin-right: 5px;
+	}
+
+	.uni-noticebar__content-wrapper {
+		flex: 1;
+		flex-direction: column;
+		overflow: hidden;
+	}
+
+	.uni-noticebar__content-wrapper--single {
+		/* #ifndef APP-NVUE */
+		line-height: 18px;
+		/* #endif */
+	}
+
+	.uni-noticebar__content-wrapper--single,
+	.uni-noticebar__content-wrapper--scrollable {
+		flex-direction: row;
+	}
+
+	/* #ifndef APP-NVUE */
+	.uni-noticebar__content-wrapper--scrollable {
+		position: relative;
+		height: 18px;
+	}
+	/* #endif */
+
+	.uni-noticebar__content--scrollable {
+		/* #ifdef APP-NVUE */
+		flex: 0;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		flex: 1;
+		display: block;
+		overflow: hidden;
+		/* #endif */
+	}
+
+	.uni-noticebar__content--single {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		flex: none;
+		width: 100%;
+		justify-content: center;
+		/* #endif */
+	}
+
+	.uni-noticebar__content-text {
+		font-size: 14px;
+		line-height: 18px;
+		/* #ifndef APP-NVUE */
+		word-break: break-all;
+		/* #endif */
+	}
+
+	.uni-noticebar__content-text--single {
+		/* #ifdef APP-NVUE */
+		lines: 1;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: block;
+		width: 100%;
+		white-space: nowrap;
+		/* #endif */
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.uni-noticebar__content-text--scrollable {
+		/* #ifdef APP-NVUE */
+		lines: 1;
+		padding-left: 750rpx;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		position: absolute;
+		display: block;
+		height: 18px;
+		line-height: 18px;
+		white-space: nowrap;
+		padding-left: 100%;
+		animation: notice 10s 0s linear infinite both;
+		animation-play-state: paused;
+		/* #endif */
+	}
+
+	.uni-noticebar__more {
+		/* #ifndef APP-NVUE */
+		display: inline-flex;
+		/* #endif */
+		flex-direction: row;
+		flex-wrap: nowrap;
+		align-items: center;
+		padding-left: 5px;
+	}
+
+	.uni-noticebar__more-text {
+		font-size: 14px;
+	}
+
+	@keyframes notice {
+		100% {
+			transform: translate3d(-100%, 0, 0);
+		}
+	}
+</style>

+ 79 - 0
components/utils/date.js

@@ -0,0 +1,79 @@
+//字符串拼接
+function strFormat(str) {
+	return str < 10 ? `0${str}` : str
+}
+// 获取当前时间
+export function currentTime() {
+	const myDate = new Date();
+	const y = myDate.getFullYear()
+	const m = myDate.getMonth() + 1;
+	const d = myDate.getDate();
+	const date = y + '-' + strFormat(m) + '-' + strFormat(d);
+
+	const hour = myDate.getHours()
+	const min = myDate.getMinutes()
+	const secon = myDate.getSeconds()
+	const time = strFormat(hour) + ':' + strFormat(min) + ':' + strFormat(secon);
+	return {
+		date,
+		time
+	}
+}
+
+//时间戳转日期
+export function timeStamp(time) {
+	const dates = new Date(time)
+	const year = dates.getFullYear()
+	const month = dates.getMonth() + 1
+	const date = dates.getDate()
+	const day = dates.getDay()
+	const hour = dates.getHours()
+	const min = dates.getMinutes()
+	const days = ['日', '一', '二', '三', '四', '五', '六']
+	return {
+		allDate: `${year}/${strFormat(month)}/${strFormat(date)}`,
+		date: `${strFormat(year)}-${strFormat(month)}-${strFormat(date)}`, //返回的日期 07-01
+		day: `星期${days[day]}`, //返回的礼拜天数  星期一
+		hour: strFormat(hour) + ':' + strFormat(min) + ':00' //返回的时钟 08:00
+	}
+}
+
+//获取最近7天的日期和礼拜天数
+export function initData() {
+	const time = []
+	const date = new Date()
+
+	const now = date.getTime() //获取当前日期的时间戳
+	let timeStr = 3600 * 24 * 1000 //一天的时间戳
+	let obj = {
+		0: "今天",
+		1: "明天",
+		2: "后天"
+	}
+	for (let i = 0; i < 7; i++) {
+		const timeObj = {}
+		timeObj.date = timeStamp(now + timeStr * i).date //保存日期
+		timeObj.timeStamp = now + timeStr * i //保存时间戳
+		timeObj.week = obj[i] ?? timeStamp(now + timeStr * i).day
+		time.push(timeObj)
+	}
+	return time
+}
+
+//时间数组
+export function initTime(startTime = '10:00:00', endTime = '21:00:00', timeInterval = 1) {
+	const time = []
+	const date = timeStamp(Date.now()).allDate
+	const startDate = `${date} ${startTime}`
+	const endDate = `${date} ${endTime}`
+	const startTimeStamp = new Date(startDate).getTime()
+	const endTimeStamp = new Date(endDate).getTime()
+	const timeStr = 3600 * 1000 * timeInterval
+	for (let i = startTimeStamp; i <= endTimeStamp; i = i + timeStr) {
+		const timeObj = {}
+		timeObj.time = timeStamp(i).hour
+		timeObj.disable = false
+		time.push(timeObj)
+	}
+	return time
+}

+ 130 - 0
components/views/app-plus/tui-share/tui-share.nvue

@@ -0,0 +1,130 @@
+<template>
+	<view class="container">
+		<text class="tui-share-title">分享到</text>
+		<view class="tui-share-list">
+			<view class="tui-share-item" hover-class="tui-hover" :hover-stay-time="150" v-for="(item, index) in shareList" :key="index">
+				<view class="tui-share-icon"><image :src="'/static/images/share/'+item.icon" class="tui-icon-app"></image></view>
+				<text class="tui-share-text">{{ item.name }}</text>
+			</view>
+		</view>
+		<view class="tui-btn-cancel" @tap="shareCancel"><text class="tui-btn-text">取消</text></view>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			//"QQ","微信","朋友圈","新浪微博"
+			shareList: [{
+				name: "QQ",
+				icon: "icon_qq.png"
+			
+			}, {
+				name: "微信",
+				icon: "icon_wechat.png"
+			}, {
+				name: "朋友圈",
+				icon: "icon_moments.png"
+			}, {
+				name: "新浪微博",
+				icon: "icon_sina.png"
+			}]
+		};
+	},
+	created() {
+		const vm = this;
+		// uni.$on('page-share', (data) => {
+		// 	vm.page = data.page;
+		// })
+	},
+	beforeDestroy() {
+		//uni.$off('page-share')
+	},
+	methods: {
+		shareCancel() {
+			const subNVue = uni.getCurrentSubNVue();
+			subNVue.hide('slide-out-bottom', 250);
+		}
+	}
+};
+</script>
+
+<style scoped>
+.container {
+	padding: 0;
+	background-color: #e8e8e8;
+}
+
+.tui-share-title {
+	font-size: 26rpx;
+	color: #7e7e7e;
+	text-align: center;
+	line-height: 26rpx;
+	padding-top: 30rpx;
+	padding-bottom: 80rpx;
+}
+
+.tui-share-list {
+	width: 750rpx;
+	padding-left: 36rpx;
+	padding-right: 36rpx;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.tui-share-item {
+	width: 126rpx;
+	align-items: center;
+}
+
+.tui-share-item:active {
+	opacity: 0.6;
+}
+
+.tui-share-icon {
+	align-items: center;
+	justify-content: center;
+	background-color: #ffffff;
+	height: 126rpx;
+	width: 126rpx;
+	border-radius: 32rpx;
+}
+
+.tui-icon-app {
+	height: 68rpx;
+	width: 68rpx;
+}
+
+.tui-share-text {
+	font-size: 24rpx;
+	line-height: 24rpx;
+	color: #7e7e7e;
+	padding-top: 20rpx;
+	padding-bottom: 20rpx;
+}
+
+.tui-btn-cancel {
+	width: 750rpx;
+	height: 100rpx;
+	position: fixed;
+	left: 0;
+	bottom: 0;
+	align-items: center;
+	justify-content: center;
+	background-color: #f6f6f6;
+	/* border-top-width: 1rpx;
+		border-top-style: solid;
+		border-top-color: #eaeef1; */
+}
+
+.tui-btn-cancle:active {
+	background-color: #eee;
+}
+
+.tui-btn-text {
+	font-size: 34rpx;
+	color: #3e3e3e;
+}
+</style>

+ 477 - 0
components/views/diyfields/diyfields.vue

@@ -0,0 +1,477 @@
+<template>
+	<view v-if="registerfield.length > 0">
+		<form @submit="bindSave">
+			<view class="tui-form">
+				<view class="tui-view-input">
+					<block v-for="(item, index) in registerfield" :key="index">
+						<block v-if="item.inputtype == 'text'">
+							<tui-list-cell :hover="false" padding="0">
+								<view class="tui-line-cell">
+									<view class="tui-title">{{ item.viewmingcheng }}</view>
+									<input v-model="item.fieldsvalue" placeholder-class="tui-phcolor" class="tui-input"
+										name="name" :placeholder="'请输入' + item.viewmingcheng" type="text" />
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype == 'textarea'">
+							<tui-list-cell :hover="false" :lineLeft="false">
+								<view class="tui-cell-input">
+									<textarea class="weui-textarea" v-model="item.fieldsvalue"
+										:placeholder="'请输入' + item.viewmingcheng" style="height: 3.3em" />
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype == 'pic'">
+							<tui-list-cell :hover="true" :lineLeft="false" :arrow="true">
+								<view @click="chooseImg(index)" class="tui-list-cell">
+									<view>{{ item.viewmingcheng }}</view>
+									<image :src="item.fieldsvalue || '/static/images/default_img.png'"
+										class="tui-avatar">
+									</image>
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype == 'pics'">
+							<tui-list-cell :hover="false" :lineLeft="false">
+								<view class="tui-img__title">{{ item.viewmingcheng }}</view>
+								<view>
+									<tui-upload :value="item.fieldsvalue" :limit="5" :params="index"
+										@complete="uploadresult" @remove="remove">
+									</tui-upload>
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype == 'lbs'">
+							<tui-list-cell @tap="onChangePosition(index)" :arrow="true" padding="0">
+								<view class="tui-line-cell">
+									<view class="tui-title"><text
+											class="tui-title-city-text">{{ item.viewmingcheng }}</text>
+									</view>
+									<input placeholder-class="tui-phcolor" class="tui-input tui-pr__30" disabled
+										:placeholder="'请选择' + item.viewmingcheng" maxlength="50" type="text"
+										v-model="item.fieldsvalue.region_name" />
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype == 'checkbox'">
+							<tui-list-cell :hover="false" :lineLeft="false">
+								<view class="uni-list">
+									<button class="ptypebut" @click="selcate" :data-index="index"
+										:data-val="checkitem.val"
+										:class="item.fieldsvalue.includes(checkitem.val.toString()) ? 'selcss' : ''"
+										type="default" v-for="checkitem in item.selectvaluearray"
+										:key="checkitem.val">{{ checkitem.key
+									}}</button>
+								</view>
+							</tui-list-cell>
+						</block>
+						<block v-if="item.inputtype === 'select'">
+							<tui-list-cell :padding="0" :hover="true" :lineLeft="false" :arrow="true">
+								<view class="tui-list-cell" style="padding: 0rpx;">
+									<picker style="width: 100%;padding: 40rpx 30rpx 40rpx 30rpx;"
+										@change="bindSelectChange" range-key="key"
+										:data-selectval="item.selectvaluearray" :data-index="index" :data-sid="item.id"
+										:range="item.selectvaluearray">
+										<view v-if="item.fieldsvalue_name" class="weui-select">
+											{{ item.fieldsvalue_name }}
+										</view>
+										<view v-else style="color: #888" class="weui-select">请选择{{ item.viewmingcheng }}
+										</view>
+									</picker>
+								</view>
+							</tui-list-cell>
+						</block>
+					</block>
+				</view>
+				<view class="tui-btn-box">
+					<button form-type="submit" class="btn86"
+						:style="'background:'+ pagestyleconfig.appstylecolor">{{submittxt}}</button>
+				</view>
+			</view>
+		</form>
+	</view>
+</template>
+
+<script>
+	const util = require("@/utils/util.js")
+	export default {
+		name: 'diyfields',
+		props: {
+			ptype: {
+				type: String,
+				default: ''
+			},
+			orderid: {
+				type: String,
+				default: ''
+			},
+			update: {
+				type: String,
+				default: ''
+			},
+			submittxt: {
+				type: String,
+				default: '提交'
+			},
+			posturl: {
+				type: String,
+				default: 'registerfield.update'
+			},
+			gourl: {
+				type: String,
+				default: '/pages/login/success'
+			},
+			gotype: {
+				type: String,
+				default: 'reLaunch'
+			}
+		},
+		data() {
+			return {
+				registerfield: {},
+				pagestyleconfig: [],
+				latitude: '',
+				longitude: ''
+			};
+		},
+		mounted() {
+			let _this = this;
+			_this.$request.get('registerfield.list', {
+				update: _this.update,
+				ptype: _this.ptype,
+				orderid: _this.orderid,
+				samkey: (new Date()).valueOf()
+			}).then(res => {
+				console.log(res);
+				if (res.errno == 0) {
+					if (res.is_submit == 1) {
+						uni.reLaunch({
+							url: "/pages/login/success?ptype=" + _this.ptype
+						});
+					} else {
+						_this.registerfield = res.data;
+						console.log(_this.registerfield);
+					}
+
+				}
+			});
+			_this.$request.post('config', {
+				mo: 'pagestyle'
+			}).then(res => {
+				if (res.errno == 0) {
+					_this.pagestyleconfig = res.data
+				}
+			});
+		},
+		watch: {},
+		methods: {
+			bindSave: function(e) {
+				var _this = this;
+				_this.$request.post(_this.posturl, {
+					update: _this.update,
+					orderid: _this.orderid,
+					registerfield: JSON.stringify(_this.registerfield)
+				}).then(res => {
+					if (res.errno != 0) {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						});
+						return;
+					} else {
+						//console.log(res.errno);
+						uni.showModal({
+							title: '提示',
+							content: res.msg,
+							showCancel: false,
+							//是否显示取消按钮 
+							success: function(res) {
+								if (res.cancel) { //点击取消,默认隐藏弹框
+								} else {
+									if (_this.gourl != 'no') {
+										if (_this.gotype == 'reLaunch') {
+											uni.reLaunch({
+												url: _this.gourl + "?ptype=" + _this.ptype
+											});
+										} else {
+											_this.sam.navigateTo(_this.gourl + "?ptype=" + _this
+												.ptype);
+												//console.log(_this.gourl);
+										}
+									}
+								}
+							}
+						});
+					}
+				});
+			},
+			selcate: function(e) {
+				var item = e.currentTarget.dataset.val.toString();
+				var index = e.currentTarget.dataset.index;
+				if (this.registerfield[index].fieldsvalue.includes(item)) {
+					this.delcateids(index, item);
+				} else {
+					this.registerfield[index].fieldsvalue.push(item);
+				}
+			},
+			bindSelectChange: function(e) {
+				if (e.detail.value) {
+					this.registerfield[e.currentTarget.dataset.index].fieldsvalue = this.registerfield[e.currentTarget
+						.dataset.index].selectvaluearray[e.detail.value].val;
+					this.registerfield[e.currentTarget.dataset.index].fieldsvalue_name = this.registerfield[e
+						.currentTarget
+						.dataset.index].selectvaluearray[e.detail.value].key;
+				}
+
+			},
+			delcateids: function(index, val) {
+				var sel = this.registerfield[index].fieldsvalue.findIndex(item => {
+					if (item == val) {
+						return true;
+					}
+				})
+				// console.log(index)
+				this.registerfield[index].fieldsvalue.splice(sel, 1)
+			},
+			chooseImg: function(key) {
+				var _this = this;
+				uni.chooseImage({
+					count: 1,
+					// 默认9
+					sizeType: ['original', 'compressed'],
+					// 可以指定是原图还是压缩图,默认二者都有
+					sourceType: ['album', 'camera'],
+					// 可以指定来源是相册还是相机,默认二者都有
+					success: function(res) {
+						// 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
+						var tempFilePaths = res.tempFilePaths;
+						_this.Imgupload(key, tempFilePaths[0]);
+					}
+				});
+			},
+			Imgupload: function(key, path) {
+				var _this = this;
+				_this.$request.uploadFile(path).then(res => {
+					_this.registerfield[key].fieldsvalue = res.url;
+				});
+			},
+			onChangePosition: function(key) {
+				const _this = this;
+				console.log('sdfasdf');
+				uni.chooseLocation({
+					success(res) {
+						_this.$request.post('geocoder.address2area', {
+							address: res.address,
+							latitude: res.latitude,
+							longitude: res.longitude
+						}).then(apires => {
+							console.log(res);
+							_this.registerfield[key].fieldsvalue = {
+								region_name: res.name,
+								address: res.address,
+
+								province_name: apires.data.province_name,
+								city_name: apires.data.city_name,
+								district_name: apires.data.district_name || '',
+
+								latitude: res.latitude,
+								longitude: res.longitude,
+							};
+						});
+					}
+				});
+			},
+			uploadresult: function(e) {
+				console.log(e)
+				this.registerfield[e.params].fieldsvalue = e.imgArr;
+			},
+			remove: function(e) {
+				//移除图片
+				//console.log(e)
+				let index = e.index
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.tui-list-cell {
+		width: 100%;
+		color: $uni-text-color;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 24rpx 60rpx 24rpx 30rpx;
+		box-sizing: border-box;
+		font-size: 30rpx;
+	}
+
+	.tui-line-cell {
+		width: 100%;
+		padding: 24rpx 30rpx;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-title {
+		color: #888888;
+		width: 180rpx;
+		font-size: 28rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-title-city-text {
+		width: 180rpx;
+		height: 40rpx;
+		display: block;
+		line-height: 46rpx;
+	}
+
+	.tui-input {
+		width: 500rpx;
+	}
+
+	.tui-avatar {
+		width: 130rpx;
+		height: 130rpx;
+		display: block;
+	}
+
+	.tui-img__title {
+		padding-bottom: 24rpx;
+	}
+
+	.uni-list {
+		width: 100%;
+		padding-top: 15rpx;
+		padding-bottom: 20rpx;
+		padding-left: 20rpx;
+		padding-right: 20rpx;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+	}
+
+	.ptypebut {
+		width: 40%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		padding-top: 5rpx;
+		padding-bottom: 5rpx;
+		margin-bottom: 10rpx;
+	}
+
+	.selcss {
+		color: #fff;
+		background: #007aff !important;
+	}
+
+	.checkboxbox {
+		padding: 20rpx;
+	}
+
+
+	.container {
+		backgroundColor: #fff;
+		padding-bottom: 80rpx;
+
+		.tui-page-title {
+			width: 100%;
+			font-size: 48rpx;
+			font-weight: bold;
+			color: $uni-text-color;
+			line-height: 42rpx;
+			padding: 110rpx 40rpx 40rpx 40rpx;
+			box-sizing: border-box;
+		}
+
+		.tui-form {
+			.tui-view-input {
+				width: 100%;
+				box-sizing: border-box;
+				padding: 0 0rpx;
+
+				.tui-cell-input {
+					width: 100%;
+					display: flex;
+					align-items: center;
+					padding-top: 48rpx;
+					padding-bottom: $uni-spacing-col-base;
+
+					input {
+						flex: 1;
+						padding-left: $uni-spacing-row-base;
+					}
+
+					.tui-icon-close {
+						margin-left: auto;
+					}
+
+					.tui-btn-send {
+						width: 156rpx;
+						text-align: right;
+						flex-shrink: 0;
+						font-size: $uni-font-size-base;
+						color: $uni-color-primary;
+					}
+
+					.tui-gray {
+						color: $uni-text-color-placeholder;
+					}
+
+					.tui-textarea {
+						width: 100%;
+						height: 300rpx;
+						font-size: 28rpx;
+						padding: 20rpx 30rpx;
+						box-sizing: border-box;
+						background-color: #fff;
+					}
+				}
+			}
+
+			.tui-cell-text {
+				width: 100%;
+				padding: 40rpx $uni-spacing-row-lg;
+				box-sizing: border-box;
+				font-size: $uni-font-size-sm;
+				color: $uni-text-color-grey;
+				display: flex;
+				align-items: center;
+
+				.tui-color-primary {
+					color: $uni-color-primary;
+					padding-left: $uni-spacing-row-sm;
+				}
+			}
+
+
+		}
+	}
+
+	.tui-btn-box {
+		width: 100%;
+		padding: 0 $uni-spacing-row-lg;
+		box-sizing: border-box;
+		margin-top: 20rpx;
+	}
+
+	.btn72 {
+		width: 100%;
+		height: 72rpx;
+		line-height: 72rpx;
+		border-radius: 98rpx;
+		color: #fff;
+	}
+
+	.btn86 {
+		width: 100%;
+		height: 86rpx;
+		line-height: 86rpx;
+		border-radius: 98rpx;
+		color: #fff;
+	}
+</style>

+ 271 - 0
components/views/diyfields/diyfieldsview.vue

@@ -0,0 +1,271 @@
+<template>
+	<view>
+		<view class="tui-form">
+			<view class="tui-view-input">
+				<block v-for="(item,index) in registerfield" :key="index">
+					<block v-if="item.inputtype == 'text' || item.inputtype == 'textarea'">
+						<tui-list-cell :hover="false" padding="0">
+							<view class="tui-line-cell">
+								<view class="tui-title">{{item.viewmingcheng}}</view>
+								<view v-if="item.fieldsvalue" class="tui-input">{{item.fieldsvalue}}</view>
+							</view>
+						</tui-list-cell>
+					</block>
+					<block v-if="item.inputtype == 'pics'">						
+						<tui-list-cell :hover="false" :lineLeft="false">
+							<view class="tui-list-cell-img">
+								<view class="img-title">{{item.viewmingcheng}}</view>
+								<image :src="item.fieldsvalue || '/static/images/default_img.png'" class="tui-avatar">
+								</image>
+							</view>
+						</tui-list-cell>
+					</block>
+					<block v-if="item.inputtype == 'lbs'">
+						<tui-list-cell padding="0">
+							<view class="tui-line-cell">
+								<view class="tui-title"><text class="tui-title-city-text">{{item.viewmingcheng}}</text>
+								</view>
+								<view class="tui-input tui-pr__30">{{item.fieldsvalue.region_name}}</view>
+							</view>
+						</tui-list-cell>
+					</block>
+					<block v-if="item.inputtype == 'checkbox'">
+						<tui-list-cell :hover="false" :lineLeft="false">
+							<view class="uni-list">
+								<block v-for="checkitem in item.selectvaluearray" :key="checkitem.val">
+									<tui-tag v-if="item.fieldsvalue.includes(checkitem.val.toString())" plain
+										size="24rpx" type="red" margin="8rpx 12rpx" padding="8rpx 12rpx">
+										{{checkitem.key}}</tui-tag>
+								</block>
+							</view>
+						</tui-list-cell>
+					</block>
+					<block v-if="item.inputtype === 'select'">
+						<tui-list-cell :padding="0" :hover="true" :lineLeft="false" :arrow="true">
+							<view class="tui-list-cell" style="padding: 0rpx;">
+								<view v-if="item.fieldsvalue_name" class="weui-select">
+									{{item.fieldsvalue_name}}
+								</view>
+							</view>
+						</tui-list-cell>
+					</block>
+				</block>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	const util = require("@/utils/util.js")
+	export default {
+		name: 'diyfields',
+		props: {
+			ptype: {
+				type: String,
+				default: ''
+			},
+			orderid: {
+				type: String,
+				default: ''
+			},
+			technicalid: {
+				type: String,
+				default: ''
+			},
+			update: {
+				type: String,
+				default: ''
+			},
+			geturl: {
+				type: String,
+				default: 'registerfield.list'
+			}
+		},
+		data() {
+			return {
+				registerfield: {},
+				latitude: '',
+				longitude: '',
+				is_submitaudit: 1
+			};
+		},
+		mounted() {
+			let _this = this;
+			_this.$request.get(_this.geturl, {
+				ptype: _this.ptype,
+				update: _this.update,
+				technicalid: _this.technicalid,
+				orderid: _this.orderid,
+				samkey: (new Date()).valueOf()
+			}).then(res => {
+				console.log(res);
+				if (res.errno == 0) {
+					_this.registerfield = res.data;
+				}
+			});
+		},
+		methods: {}
+	}
+</script>
+<style lang="scss" scoped>
+	.tui-list-cell {
+		width: 100%;
+		color: $uni-text-color;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 24rpx 60rpx 24rpx 30rpx;
+		box-sizing: border-box;
+		font-size: 30rpx;
+	}
+	.tui-list-cell-img {
+		width: 100%;
+		color: $uni-text-color;
+		box-sizing: border-box;
+		font-size: 30rpx;
+	}
+	.img-title {
+		padding-bottom: 20rpx;
+	}
+	.tui-line-cell {
+		width: 100%;
+		padding: 24rpx 30rpx;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-title {
+		width: 180rpx;
+		font-size: 28rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-title-city-text {
+		width: 180rpx;
+		height: 40rpx;
+		display: block;
+		line-height: 46rpx;
+	}
+
+	.tui-input {
+		width: 500rpx;
+	}
+
+	.tui-avatar {
+		width: 130rpx;
+		height: 130rpx;
+		display: block;
+	}
+
+	.uni-list {
+		width: 100%;
+		padding-top: 15rpx;
+		padding-bottom: 20rpx;
+		padding-left: 20rpx;
+		padding-right: 20rpx;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+	}
+
+	.ptypebut {
+		width: 40%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		padding-top: 15rpx;
+		padding-bottom: 15rpx;
+		margin-bottom: 10rpx;
+	}
+
+	.checkboxbox {
+		padding: 20rpx;
+	}
+
+	.container {
+		backgroundColor: #fff;
+		padding-bottom: 80rpx;
+
+		.tui-page-title {
+			width: 100%;
+			font-size: 48rpx;
+			font-weight: bold;
+			color: $uni-text-color;
+			line-height: 42rpx;
+			padding: 110rpx 40rpx 40rpx 40rpx;
+			box-sizing: border-box;
+		}
+
+		.tui-form {
+			.tui-view-input {
+				width: 100%;
+				box-sizing: border-box;
+				padding: 0 0rpx;
+
+				.tui-cell-input {
+					width: 100%;
+					display: flex;
+					align-items: center;
+					padding-top: 48rpx;
+					padding-bottom: $uni-spacing-col-base;
+
+					input {
+						flex: 1;
+						padding-left: $uni-spacing-row-base;
+					}
+
+					.tui-icon-close {
+						margin-left: auto;
+					}
+
+					.tui-btn-send {
+						width: 156rpx;
+						text-align: right;
+						flex-shrink: 0;
+						font-size: $uni-font-size-base;
+						color: $uni-color-primary;
+					}
+
+					.tui-gray {
+						color: $uni-text-color-placeholder;
+					}
+
+					.tui-textarea {
+						width: 100%;
+						height: 300rpx;
+						font-size: 28rpx;
+						padding: 20rpx 30rpx;
+						box-sizing: border-box;
+						background-color: #fff;
+					}
+				}
+			}
+
+			.tui-cell-text {
+				width: 100%;
+				padding: 40rpx $uni-spacing-row-lg;
+				box-sizing: border-box;
+				font-size: $uni-font-size-sm;
+				color: $uni-text-color-grey;
+				display: flex;
+				align-items: center;
+
+				.tui-color-primary {
+					color: $uni-color-primary;
+					padding-left: $uni-spacing-row-sm;
+				}
+			}
+
+			.tui-btn-box {
+				width: 100%;
+				padding: 0 $uni-spacing-row-lg;
+				box-sizing: border-box;
+				margin-top: 20rpx;
+			}
+		}
+		
+	}
+</style>

+ 30 - 0
components/views/diyitem/blank.vue

@@ -0,0 +1,30 @@
+<template>
+	<view class="diy-blank"
+		:style="{ height: diyitem.base.height + 'px', background: diyitem.base.bc }">
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'blank',
+		props: {
+			diyitem: {
+				type: Object,
+				default () {
+					return {};
+				}
+			}
+		},
+		computed: {
+
+		},
+		data() {
+			return {};
+		},
+		methods: {
+		}
+	};
+</script>
+<style>
+	@import './diyapge.css';
+</style>

+ 73 - 0
components/views/diyitem/coupon.vue

@@ -0,0 +1,73 @@
+<template>
+	<view class="diy-coupon" :style="diyitem.base.bgstyle">
+		<scroll-view scroll-x>
+			<view class="coupon-wrapper" v-for="(dataItem, index) in diyitem.list" :key="index">
+				<view :class="['coupon-item', 'color__' + dataItem.color]">
+					<i class="before" :style="{ 'background': diyitem.base.bc }"></i>
+					<view class="left-content dis-flex flex-dir-column flex-x-center flex-y-center">
+						<view class="content-top">
+							<block v-if="dataItem.coupon_type == 10">
+								<text class="f-30">¥</text>
+								<text class="price">{{ dataItem.reduce_price }}</text>
+							</block>
+							<text class="price" v-if="dataItem.coupon_type == 20">{{dataItem.discount }}折</text>
+						</view>
+						<view class="content-bottom">
+							<text class="f-24">满{{ dataItem.min_price }}元可用</text>
+						</view>
+					</view>
+					<view class="right-receive dis-flex flex-x-center flex-y-center">
+						<view v-if="dataItem.state.value" :data-itemindex="itemindex"
+							:data-index="dataItem.coupon_id" :data-state="dataItem.state.value"
+							:data-coupon-id="dataItem.coupon_id" @tap="receiveTap"
+							class="dis-flex flex-dir-column">
+							<text>立即</text>
+							<text>领取</text>
+						</view>
+						<view v-else class="state">
+							<text>{{ dataItem.state.text }}</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'coupon',
+		props: {
+			diyitem: {
+				type: Object,
+				default () {
+					return {};
+				}
+			},
+			itemindex: {
+				type: Number,
+				default: 0
+			}
+		},
+		computed: {
+
+		},
+		data() {
+			return {};
+		},
+		methods: {
+			navigateTo: function(e) {
+				this.sam.diynavigateTo(e)
+			},
+			/**
+			 * 领取优惠券
+			 */
+			receiveTap: function(e) {
+				this.$emit("receiveTap", e)
+			},
+		}
+	};
+</script>
+<style>
+	@import './diyapge.css';
+</style>

+ 1264 - 0
components/views/diyitem/diyapge.css

@@ -0,0 +1,1264 @@
+.container {
+	color: #333;
+}
+
+.diybox {
+	padding-bottom: 186rpx;
+}
+
+.topheight {
+	color: #ffffff;
+}
+
+.tui-header-box {
+	width: 100%;
+	position: fixed;
+	left: 0;
+	top: 0;
+	z-index: 995;
+}
+
+/*居中
+.tui-header {
+	width: 100%;
+	padding-left: 26rpx;
+	font-size: 18px;
+	line-height: 18px;
+	font-weight: 500;
+	height: 32px;
+	display: flex;
+	align-items: center;
+	justify-content: left;
+}*/
+.tui-header {
+	width: 100%;
+	font-size: 18px;
+	line-height: 18px;
+	font-weight: 500;
+	height: 32px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.orderplay {
+	width: 100%;
+	height: 100rpx;
+	padding: 0 20rpx;
+	box-sizing: border-box;
+	position: fixed;
+	left: 0;
+	top: 180rpx;
+	z-index: 999;
+	transition: opacity .4s;
+}
+
+.housingestate {
+	font-size: 28rpx;
+	padding-left: 30rpx;
+	padding-right: 30rpx;
+}
+
+/*banner*/
+.bannerbox {
+	box-sizing: border-box;
+}
+
+.bannerbottom {
+	padding-top: 20rpx;
+}
+
+.tui-banner-bg {
+	display: flex;
+	position: relative;
+}
+
+.tui-banner-box {
+	width: 100%;
+	padding: 0 20rpx;
+	box-sizing: border-box;
+	position: absolute;
+	/* overflow: hidden; */
+	z-index: 99;
+	left: 0;
+}
+
+.tui-banner-swiper {
+	width: 100%;
+	border-radius: 20rpx;
+	overflow: hidden;
+	transform: translateY(0);
+	background-color: #f8f8f8;
+}
+
+.tui-slide-image {
+	width: 100%;
+	display: block;
+}
+
+/* #ifdef MP-WEIXIN */
+.tui-banner-swiper .wx-swiper-dot {
+	width: 8rpx;
+	height: 8rpx;
+	display: inline-flex;
+	background: none;
+	justify-content: space-between;
+}
+
+.tui-banner-swiper .wx-swiper-dot::before {
+	content: '';
+	flex-grow: 1;
+	background-color: rgba(255, 255, 255, 0.8);
+	border-radius: 16rpx;
+	overflow: hidden;
+}
+
+.tui-banner-swiper .wx-swiper-dot-active::before {
+	background-color: #fff;
+}
+
+.tui-banner-swiper .wx-swiper-dot.wx-swiper-dot-active {
+	width: 16rpx;
+}
+
+/* #endif */
+
+/* #ifndef MP-WEIXIN */
+>>>.tui-banner-swiper .uni-swiper-dot {
+	width: 8rpx;
+	height: 8rpx;
+	display: inline-flex;
+	background-color: none;
+	justify-content: space-between;
+}
+
+>>>.tui-banner-swiper .uni-swiper-dot::before {
+	content: '';
+	flex-grow: 1;
+	background-color: rgba(255, 255, 255, 0.8);
+	border-radius: 16rpx;
+	overflow: hidden;
+}
+
+>>>.tui-banner-swiper .uni-swiper-dot-active::before {
+	background-color: #fff;
+}
+
+>>>.tui-banner-swiper .uni-swiper-dot.uni-swiper-dot-active {
+	width: 16rpx;
+}
+
+/* #endif */
+
+.tui-product-category {
+	padding: 80rpx 20rpx 30rpx 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-wrap: wrap;
+	font-size: 24rpx;
+	color: #555;
+	/* margin-bottom: 20rpx; */
+}
+
+.tui-category-item {
+	width: 20%;
+	height: 118rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-direction: column;
+	padding-top: 30rpx;
+}
+
+.tui-category-img {
+	height: 88rpx;
+	width: 88rpx;
+	display: block;
+}
+
+.tui-category-name {
+	line-height: 24rpx;
+}
+
+.tui-block__box {
+	width: 100%;
+	box-sizing: border-box;
+	background-color: #ffffff;
+	border-radius: 20rpx;
+	overflow: hidden;
+}
+
+.tui-product-box {
+	margin-top: 20rpx;
+	padding: 0 25rpx;
+	box-sizing: border-box;
+}
+
+.tui-img__coupon {
+	width: 100%;
+	height: 184rpx;
+	display: block;
+}
+
+.tui-mtop__20 {
+	margin-top: 20rpx;
+}
+
+.tui-bg-white {
+	background-color: #fff;
+}
+
+.group-name-box {
+	padding-left: 25rpx;
+	padding-right: 25rpx;
+}
+
+.tui-group-name {
+	width: 100%;
+	font-size: 34rpx;
+	line-height: 34rpx;
+	font-weight: bold;
+	text-align: center;
+	padding: 30rpx 0 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	color: #333;
+}
+
+.tui-group-list {
+	padding-left: 10rpx;
+	padding-right: 10rpx;
+	justify-content: space-between;
+	box-sizing: border-box;
+	/* padding-top: 20rpx; */
+}
+
+.tui-sub__desc {
+	color: #34c7a9;
+	font-size: 28rpx;
+	font-weight: 400;
+	padding-left: 25rpx;
+}
+
+.tui-seckill__box {
+	display: flex;
+	align-items: center;
+}
+
+.tui-seckill__img {
+	width: 40rpx;
+}
+
+.tui-countdown__box {
+	width: 228rpx;
+	display: flex;
+	align-items: center;
+
+	color: #fff;
+	background-color: #fff;
+	font-weight: 400;
+	height: 40rpx;
+	border-radius: 30px;
+	overflow: hidden;
+	margin-left: 25rpx;
+}
+
+.tui-countdown__title {
+	width: 100rpx;
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-shrink: 0;
+	font-size: 24rpx;
+	line-height: 24rpx;
+}
+
+.tui-flex__center {
+	flex: 1;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+/*秒杀商品*/
+.tui-goods__list {
+	display: flex;
+	align-items: center;
+}
+
+.tui-goods__item {
+	background-color: #fff;
+	width: 150rpx;
+	height: 230rpx;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	flex-shrink: 0;
+	margin-right: 18rpx;
+}
+
+.tui-goods__imgbox {
+	width: 150rpx;
+	height: 150rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	box-sizing: border-box;
+}
+
+.tui-goods__img {
+	max-width: 150rpx;
+	max-height: 150rpx;
+	display: block;
+}
+
+.tui-pri__box {
+	max-width: 150rpx;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.tui-sale-pri {
+	display: flex;
+	align-items: flex-end;
+	padding: 10rpx 0 8rpx;
+	box-sizing: border-box;
+	font-size: 28rpx;
+	line-height: 28rpx;
+	color: #eb0909;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+
+.tui-size-sm {
+	font-size: 24rpx;
+	line-height: 24rpx;
+	transform: scale(0.8);
+	transform-origin: 0 50%;
+}
+
+.tui-original__pri {
+	font-size: 24rpx;
+	line-height: 24rpx;
+	color: #999999;
+	transform-origin: center 10%;
+	transform: scale(0.8);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	text-decoration: line-through;
+}
+
+/*秒杀商品*/
+
+.tui-more__box {
+	display: flex;
+	align-items: center;
+	font-weight: 400;
+	color: #999;
+}
+
+.tui-more__box text {
+	font-size: 24rpx;
+	line-height: 24rpx;
+}
+
+.tui-new-box {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-wrap: wrap;
+}
+
+.tui-new-item {
+	width: 49%;
+	height: 180rpx;
+	padding: 0 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	background: #f5f2f9;
+	position: relative;
+	border-radius: 12rpx;
+}
+
+.tui-new-mtop {
+	margin-top: 2%;
+}
+
+.tui-title-box {
+	font-size: 24rpx;
+}
+
+.tui-new-title {
+	line-height: 32rpx;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+.tui-new-price {
+	padding-top: 18rpx;
+}
+
+.tui-new-present {
+	color: #ff201f;
+	font-weight: bold;
+}
+
+.tui-new-original {
+	display: inline-block;
+	color: #a0a0a0;
+	text-decoration: line-through;
+	padding-left: 12rpx;
+	transform: scale(0.8);
+	transform-origin: center center;
+}
+
+.tui-new-img {
+	width: 148rpx;
+	height: 148rpx;
+	display: block;
+	flex-shrink: 0;
+}
+
+.tui-new-label {
+	width: 56rpx;
+	height: 56rpx;
+	border-top-left-radius: 12rpx;
+	position: absolute;
+	left: 0;
+	top: 0;
+}
+
+.tui-title__img {
+	width: 100%;
+	padding: 30rpx 0;
+	display: flex;
+	justify-content: center;
+}
+
+.tui-title__img image {
+	width: 352rpx;
+	height: 32rpx;
+}
+
+.tui-product-list {
+	padding-left: 10rpx;
+	padding-right: 10rpx;
+	justify-content: space-between;
+	box-sizing: border-box;
+	/* padding-top: 20rpx; */
+}
+
+.diy-duo-list {
+	padding-left: 10rpx;
+	padding-right: 10rpx;
+	justify-content: space-between;
+	box-sizing: border-box;
+}
+
+.tui-product-container {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+}
+
+.tui-pro-item:last-child {
+	margin-right: 0;
+}
+
+.tui-pro-item {
+	margin-left: 1%;
+	margin-right: 1%;
+	width: 48%;
+	margin-bottom: 2%;
+	background: #fff;
+	box-sizing: border-box;
+	border-radius: 12rpx;
+	overflow: hidden;
+}
+
+.tui-pro-img {
+	width: 100%;
+	display: block;
+}
+
+.tui-pro-content {
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+	box-sizing: border-box;
+	padding: 20rpx;
+}
+
+.tui-pro-tit {
+	color: #2e2e2e;
+	font-size: 26rpx;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+.tui-pro-price {
+	padding-top: 18rpx;
+}
+
+.tui-sale-price {
+	font-size: 34rpx;
+	font-weight: 500;
+	color: #e41f19;
+}
+
+.tui-factory-price {
+	font-size: 24rpx;
+	color: #a0a0a0;
+	text-decoration: line-through;
+	padding-left: 12rpx;
+}
+
+.tui-pro-pay {
+	padding-top: 10rpx;
+	font-size: 24rpx;
+	color: #656565;
+}
+
+
+
+
+/* 多图组 */
+.diy-duo-box {
+	padding: 0 25rpx;
+	box-sizing: border-box;
+}
+
+.diy-duo {
+	/*padding-top: 20rpx;
+		padding-bottom: 26rpx;*/
+}
+
+.diy-duo .duo-list {
+	box-sizing: border-box;
+}
+
+.diy-duo .duo-list .duo-item {
+	box-sizing: border-box;
+	padding: 8rpx;
+	text-align: center;
+}
+
+.diy-duo .duo-list.display__slide {
+	white-space: nowrap;
+	font-size: 0;
+}
+
+.diy-duo .duo-list.display__slide .duo-item {
+	display: inline-block;
+}
+
+.diy-duo .duo-list.display__list .duo-item {
+	float: left;
+}
+
+.diy-duo .duo-list.column__1 .duo-item {
+	width: 100%;
+}
+
+.diy-duo .duo-list.column__2 .duo-item {
+	width: 50%;
+}
+
+.diy-duo .duo-list.column__2 .duo-image image {
+	width: 100%;
+}
+
+.diy-duo .duo-list.column__3 .duo-item {
+	width: 33.33333%;
+}
+
+.diy-duo .duo-list.column__3 .duo-image image {
+	width: 100%;
+}
+
+.diy-duo .duo-list.column__4 .duo-item {
+	width: 25%;
+}
+
+.diy-duo .duo-list.column__4 .duo-image image {
+	width: 100%;
+}
+
+.diy-duo .duo-list.column__5 .duo-item {
+	width: 20%;
+}
+
+.diy-duo .duo-list.column__5 .duo-image image {
+	width: 100%;
+}
+
+/*
+	.diy-duo .duo-list .duo-item .duo-image image {
+		display: block;
+		border-radius: 12rpx 12rpx 0rpx 0rpx;
+		width: 100%;
+	}
+*/
+.diy-duo .duo-list .duo-item .duo-name {
+	font-size: 28rpx;
+	line-height: 50rpx;
+	white-space: normal;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+
+
+/* 图文组 */
+
+.tui-tuwen-box {
+	margin-top: 20rpx;
+	padding: 0 25rpx;
+	box-sizing: border-box;
+}
+
+.item-tuwen-style1 {
+	width: 100%;
+	box-sizing: border-box;
+	background-color: #ffffff;
+	border-radius: 20rpx;
+	overflow: hidden;
+}
+
+.item-tuwen-style1 .tui-tuwen-title {
+	padding-left: 25rpx;
+	padding-right: 25rpx;
+	width: 100%;
+	font-size: 34rpx;
+	line-height: 34rpx;
+	font-weight: bold;
+	text-align: left;
+	padding: 30rpx 20rpx 20rpx 20rpx;
+	display: flex;
+	align-items: left;
+	color: #333;
+}
+
+.diy-tuwen {
+	width: 100%;
+}
+
+.diy-tuwen .item-image {
+	width: 100%;
+	height: 300rpx;
+	position: relative;
+	overflow: hidden;
+}
+
+.diy-tuwen .item-image image {
+	display: block;
+	height: 150px;
+	width: 100%;
+}
+
+.item-tuwen-style1 .diy-tuwen .item-image .note {
+	line-height: 18px;
+	color: #fff;
+	background: rgba(0, 0, 0, .5);
+	position: absolute;
+	bottom: 0;
+	padding: 12rpx 20rpx;
+	width: 100%;
+}
+
+.item-tuwen-style2 {
+	width: 100%;
+	padding-bottom: 20rpx;
+	box-sizing: border-box;
+	background-color: #ffffff;
+	border-radius: 20rpx;
+	overflow: hidden;
+}
+
+.item-tuwen-style2 .tui-tuwen-title {
+	white-space: nowrap;
+	padding-left: 20rpx;
+	font-weight: bold;
+	padding-top: 10px;
+	text-align: left;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.item-tuwen-style2 .diy-tuwen .note {
+	padding-left: 20rpx;
+	padding-top: 5px;
+	line-height: 20px;
+	color: #999;
+}
+
+/* 图片橱窗 */
+
+.diy-window .data-list::after {
+	clear: both;
+	content: " ";
+	display: table;
+}
+
+.diy-window .data-list .data-item {
+	float: left;
+	box-sizing: border-box;
+}
+
+.diy-window .data-list image {
+	display: block;
+	width: 100%;
+}
+
+/* 分列布局 */
+
+.diy-window .avg-sm-2>.data-item {
+	width: 50%;
+}
+
+.diy-window .avg-sm-3>.data-item {
+	width: 33.33333333%;
+}
+
+.diy-window .avg-sm-4>.data-item {
+	width: 25%;
+}
+
+.diy-window .avg-sm-5>.data-item {
+	width: 20%;
+}
+
+/* 橱窗样式 */
+
+.diy-window .display {
+	height: 0;
+	width: 100%;
+	margin: 0;
+	padding-bottom: 50%;
+	position: relative;
+	box-sizing: border-box;
+}
+
+.diy-window .display view {
+	box-sizing: border-box;
+}
+
+.diy-window .display image {
+	width: 100%;
+	height: 100%;
+}
+
+.diy-window .display .display-left {
+	width: 50%;
+	height: 100%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+}
+
+.diy-window .display .display-right {
+	width: 50%;
+	height: 100%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 0;
+	left: 50%;
+}
+
+.diy-window .display .display-right1 {
+	width: 100%;
+	height: 50%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+}
+
+.diy-window .display .display-right2 {
+	width: 100%;
+	height: 50%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 50%;
+	left: 0;
+}
+
+.diy-window .display .display-right2 .left {
+	width: 50%;
+	height: 100%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+}
+
+.diy-window .display .display-right2 .right {
+	width: 50%;
+	height: 100%;
+	overflow-y: hidden;
+	position: absolute;
+	top: 0;
+	left: 50%;
+}
+
+
+/* 公告组 */
+
+.notice__icon {
+	font-size: 0;
+}
+
+.notice__icon img {
+	width: 28rpx;
+	height: 28rpx;
+}
+
+.notice__text {
+	width: 298rpx;
+	height: 30rpx;
+	padding-left: 5rpx;
+}
+
+
+
+/* 优惠券 */
+.diy-coupon {
+	white-space: nowrap;
+	font-size: 0;
+}
+
+.diy-coupon .coupon-item {
+	width: 350rpx;
+	height: 130rpx;
+	position: relative;
+	color: #fff;
+	overflow: hidden;
+	box-sizing: border-box;
+}
+
+.diy-coupon .coupon-item i.before {
+	content: "";
+	position: absolute;
+	z-index: 1;
+	width: 40rpx;
+	height: 40rpx;
+	top: 50%;
+	left: -.8rem;
+	-webkit-transform: translateY(-50%);
+	transform: translateY(-50%);
+	-webkit-border-radius: 80%;
+	border-radius: 80%;
+	background-color: #fff;
+}
+
+.diy-coupon .coupon-wrapper {
+	display: inline-block;
+	padding: 0 12rpx;
+}
+
+.diy-coupon .coupon-item .left-content {
+	position: relative;
+	width: 70%;
+	height: 100%;
+	background-color: #e5004f;
+	float: left;
+}
+
+.diy-coupon .coupon-item .left-content .content-top .price {
+	font-size: 44rpx;
+}
+
+.diy-coupon .coupon-item.color__blue .left-content {
+	background: linear-gradient(-125deg, #57bdbf, #2f9de2);
+}
+
+.diy-coupon .coupon-item.color__red .left-content {
+	background: linear-gradient(-128deg, #ff6d6d, #ff3636);
+}
+
+.diy-coupon .coupon-item.color__violet .left-content {
+	background: linear-gradient(-113deg, #ef86ff, #b66ff5);
+}
+
+.diy-coupon .coupon-item.color__yellow .left-content {
+	background: linear-gradient(-141deg, #f7d059, #fdb054);
+}
+
+.diy-coupon .coupon-item.color__gray .left-content {
+	background: linear-gradient(-113deg, #bdbdbd, #a2a1a2);
+}
+
+.diy-coupon .coupon-item.color__gray .right-receive {
+	background-color: #949494;
+}
+
+.diy-coupon .coupon-item .right-receive {
+	width: 30%;
+	height: 100%;
+	background-color: #4e4e4e;
+	text-align: center;
+	float: right;
+}
+
+.diy-coupon .coupon-item .right-receive text {
+	font-size: 26rpx;
+}
+
+/* 商品组 */
+
+.diy-goods .goods-list {
+	padding: 4rpx;
+	box-sizing: border-box;
+}
+
+.diy-goods .goods-list .goods-item {
+	box-sizing: border-box;
+	padding: 8rpx;
+	margin-bottom: 3rpx;
+}
+
+.diy-goods .goods-list.display__slide {
+	white-space: nowrap;
+	font-size: 0;
+}
+
+.diy-goods .goods-list.display__slide .goods-item {
+	display: inline-block;
+}
+
+.diy-goods .goods-list.display__list .goods-item {
+	float: left;
+}
+
+.diy-goods .goods-list.column__1 .goods-item {
+	width: 100%;
+}
+
+.diy-goods .goods-list.column__2 .goods-item {
+	width: 50%;
+}
+
+.diy-goods .goods-list.column__2 .goods-image image {
+	width: 359rpx;
+	height: 359rpx;
+}
+
+.diy-goods .goods-list.column__3 .goods-item {
+	width: 33.33333%;
+}
+
+.diy-goods .goods-list.column__3 .goods-image image {
+	width: 235.3rpx;
+	height: 235.3rpx;
+}
+
+.diy-goods .goods-list .goods-item .goods-image image {
+	display: block;
+	border-radius: 12rpx 12rpx 0rpx 0rpx;
+	width: 100%;
+}
+
+.diy-goods .goods-list .goods-item .detail {
+	padding: 8rpx;
+	/*height: 128rpx;*/
+	background: #fff;
+	border-radius: 0rpx 0rpx 12rpx 12rpx;
+	overflow: hidden;
+}
+
+.diy-goods .goods-list .goods-item .goods-price {
+	color: #e41f19;
+	font-size: 38rpx;
+}
+
+.diy-goods .goods-list .goods-item .detail .goods-name {
+	white-space: normal;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+.diy-goods .goods-list .goods-item .technicaldetail {
+	padding: 8rpx;
+	background: #fff;
+	border-radius: 0rpx 0rpx 12rpx 12rpx;
+	overflow: hidden;
+}
+
+.diy-goods .goods-list .goods-item .technicaldetail .goods-name {
+	white-space: normal;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+.diy-goods .goods-list .goods-item .groupdetail {
+	padding: 8rpx;
+	height: 196rpx;
+	background: #fff;
+	border-radius: 0rpx 0rpx 12rpx 12rpx;
+	overflow: hidden;
+}
+
+.diy-goods .goods-list .goods-item .groupdetail .goods-name {
+	white-space: normal;
+	word-break: break-all;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+/*商品列表*/
+
+.tui-goodli-box {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-wrap: wrap;
+}
+
+.tui-goodli-item {
+	width: 100%;
+	height: 180rpx;
+	padding: 0 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	background: #fff;
+	position: relative;
+	border-radius: 12rpx;
+}
+
+.tui-goodli-mtop {
+	margin-top: 2%;
+}
+
+.goodliimgtitle {
+	width: 71%;
+	display: flex;
+}
+
+.tui-goodli-img {
+	width: 40%;
+	display: block;
+	border-radius: 12rpx;
+}
+
+.tui-goodli-title-box {
+	width: 60%;
+	padding-left: 20rpx;
+	font-size: 24rpx;
+}
+
+
+.goodli-butt-box {
+	margin-top: 30rpx;
+	padding: 20rpx;
+	width: 28%;
+	display: block;
+	border-radius: 12rpx;
+}
+
+.goodli-button {
+	font-size: 24rpx;
+	color: #ffffff;
+	align-items: center;
+}
+
+.tui-goodli-title {
+	font-size: 30rpx;
+	padding-top: 2rpx;
+	word-break: break-all;
+	font-weight: bold;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+}
+
+.goodli-keyword {
+	padding-top: 5rpx;
+	color: #999;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 1;
+}
+
+.tui-goodli-price {
+	padding-top: 8rpx;
+}
+
+.tui-goodli-present {
+	color: #ff201f;
+	font-size: 38rpx;
+}
+
+.tui-goodli-label {
+	width: 56rpx;
+	height: 56rpx;
+	border-top-left-radius: 12rpx;
+	position: absolute;
+	left: 0;
+	top: 0;
+}
+
+/*商品列表end*/
+.tui-group-btn {
+	max-width: 312rpx;
+	height: 48rpx;
+	border-radius: 6rpx;
+	display: flex;
+	align-items: center;
+	padding: 4rpx;
+	margin-top: 10rpx;
+	box-sizing: border-box;
+}
+
+.tui-flex-btn {
+	height: 100%;
+	flex: 1;
+	text-align: center;
+	font-size: 26rpx;
+	line-height: 26rpx;
+	font-weight: 400;
+	color: #fff;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.tui-flex-btn:first-child {
+	background: #fff;
+}
+
+/**客服**/
+.img-tel-style {
+	height: 80rpx;
+	width: 80rpx;
+	position: fixed;
+	bottom: 330rpx;
+	right: 16rpx;
+	text-align: center;
+	border-radius: 50%;
+	background-color: #fff;
+	border: 1rpx solid #ddd;
+	opacity: 0.9
+}
+
+.img-plus-style {
+	height: 80rpx;
+	width: 80rpx;
+	position: fixed;
+	bottom: 230rpx;
+	right: 16rpx;
+	text-align: center;
+	border-radius: 50%;
+	background-color: #fff;
+	border: 1rpx solid #ddd;
+	opacity: 0.9
+}
+
+.zindex100 {
+	z-index: 100
+}
+
+.yc {
+	opacity: 0
+}
+
+/**客服end**/
+/*客服二维modal弹层*/
+.tui-poster__canvas {
+	background-color: #fff;
+	position: absolute;
+	left: -9999px;
+}
+
+.tui-poster__box {
+	width: 100%;
+	position: relative;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	flex-direction: column;
+}
+
+.tui-close__img {
+	width: 48rpx;
+	height: 48rpx;
+	position: absolute;
+	right: 0;
+	top: -60rpx;
+}
+
+.tui-poster__img {
+	width: 560rpx;
+	height: 890rpx;
+	border-radius: 20rpx;
+	margin-bottom: 40rpx;
+}
+
+/*客服二维modal弹层end*/
+
+.tui-modal-custom {
+	padding-top: 60rpx;
+	padding-bottom: 50rpx;
+	text-align: center;
+}
+
+.tui-prompt-title {
+	padding-bottom: 20rpx;
+	font-size: 34rpx;
+}
+
+.tui-modal__btn {
+	align-items: center;
+	justify-content: space-between;
+}
+
+.tui-box {
+	padding: 15rpx 20rpx;
+	box-sizing: border-box;
+}
+
+.tui-btn-danger {
+	height: 80rpx;
+	line-height: 80rpx;
+	background: #eb0909 !important;
+	border-radius: 98rpx;
+	color: #fff;
+}

+ 39 - 0
components/views/diyitem/diyvideo.vue

@@ -0,0 +1,39 @@
+<template>
+	<view class="diy-video" :style="{ paddingTop: diyitem.base.paddingTop + 'px', paddingLeft: 0 }">
+		<video :style="{ height: diyitem.base.height + 'px' }" :src="diyitem.videoUrl"
+			:poster="diyitem.img" controls></video>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'video',
+		props: {
+			diyitem: {
+				type: Object,
+				default () {
+					return {};
+				}
+			}
+		},
+		computed: {
+
+		},
+		data() {
+			return {};
+		},
+		methods: {
+			navigateTo: function(e) {
+				this.sam.diynavigateTo(e)
+			}
+		}
+	};
+</script>
+<style>
+	/* 视频 */
+	.diy-video video {
+		width: 100%;
+		display: block;
+	}
+	
+</style>

+ 70 - 0
components/views/diyitem/duo.vue

@@ -0,0 +1,70 @@
+<template>
+	<view class="diy-duo-box">
+		<view class="tui-block__box" :style="diyitem.base.bgstyle">
+			<view v-if="diyitem.title.title.show" class="group-name-box">
+				<view class="tui-group-name">
+					<view>
+						<text>{{ diyitem.title.title.txt }}</text>
+					</view>
+					<view v-if="diyitem.title.more.show" class="tui-more__box" @tap="navigateTo"
+						:data-url="diyitem.title.link">
+						<text>{{ diyitem.title.more.txt }}</text>
+						<tui-icon name="arrowright" :size="36" unit="rpx" color="#999"></tui-icon>
+					</view>
+				</view>
+			</view>
+			<view v-if="diyitem.list.length > 0" class="diy-duo-list">
+				<view class="diy-duo">
+					<view
+						:class="'duo-list display__' + diyitem.base.display + ' column__' + diyitem.base.column">
+						<scroll-view :scroll-x="diyitem.base.display === 'slide' ? true : false">
+							<block v-for="(dataItem, index) in diyitem.list" :key="index">
+								<view class="duo-item" @tap="navigateTo" :data-url="dataItem.link">
+									<view v-if="dataItem.img" class="duo-image">
+										<image mode="widthFix"
+											:style="'border-radius:' + diyitem.base.borderradius + '%'"
+											:src="dataItem.img">
+										</image>
+									</view>
+									<view v-if="dataItem.text.show" class="duo-name twolist-hidden f-28"
+										:style="{ color: dataItem.text.color }">
+										{{ dataItem.text.txt }}
+									</view>
+								</view>
+							</block>
+	
+						</scroll-view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'duo',
+		props: {
+			diyitem: {
+				type: Object,
+				default () {
+					return {};
+				}
+			}
+		},
+		computed: {
+
+		},
+		data() {
+			return {};
+		},
+		methods: {
+			navigateTo: function(e) {
+				this.sam.diynavigateTo(e)
+			}
+		}
+	};
+</script>
+<style>
+	@import './diyapge.css';
+</style>

+ 153 - 0
components/views/diyitem/goods.vue

@@ -0,0 +1,153 @@
+<template>
+	<view class="tui-product-box">
+		<view class="tui-block__box" :style="diyitem.base.bgstyle">
+			<view v-if="diyitem.title.title.show" class="group-name-box">
+				<view class="tui-group-name">
+					<view>
+						<text>{{ diyitem.title.title.txt }}</text>
+					</view>
+					<view v-if="diyitem.title.more.show" class="tui-more__box" @tap="navigateTo"
+						:data-url="diyitem.title.link">
+						<text>{{ diyitem.title.more.txt }}</text>
+						<tui-icon name="arrowright" :size="36" unit="rpx" color="#999"></tui-icon>
+					</view>
+				</view>
+			</view>
+			<view v-if="diyitem.list.length > 0" class="tui-product-list">
+				<view v-if="diyitem.base.display === 'li'">
+					<view class="tui-goodli-box">
+						<block v-for="(dataItem, index) in diyitem.list" :key="index">
+							<view v-if="dataItem.goods_id > 0" class="tui-goodli-item">
+								<view class="goodliimgtitle" @tap="goodsDetail" :data-id="dataItem.goods_id">
+									<image :src="dataItem.image" class="tui-goodli-img" mode="widthFix">
+									</image>
+									<view class="tui-goodli-title-box">
+										<view class="tui-goodli-title">{{ dataItem.goods_name }}
+										</view>
+										<view v-if="dataItem.keyword" class="goodli-keyword">
+											{{ dataItem.keyword }}
+										</view>
+										<view class="tui-goodli-price">
+											<text class="tui-goodli-present">
+												<text class="f-24">¥</text>{{ dataItem.price}}
+												<block v-if="dataItem.ptype==2">
+													<text v-if="dataItem.time_amount > 0"
+														class="f-24">/{{dataItem.time_amount}}{{dataItem.quantity_unit}}</text>
+													<text v-else class="f-24">/<text
+															v-if="dataItem.is_times">{{dataItem.timesmum}}</text>次</text>
+												</block>
+											</text>
+											<text v-if="dataItem.original_price > 0" class="tui-factory-price">¥{{ dataItem.original_price
+												}}</text>
+										</view>
+									</view>
+								</view>
+								<view v-if="diyitem.base.is_binding == true" class="goodli-butt-box">
+									<button :data-id="dataItem.goods_id" @click="choosetechnical"
+										:style="'background:'+ pagestyleconfig.appstylecolor"
+										class="goodli-button">选择{{lang.technical}}</button>
+								</view>
+								<view v-else class="goodli-butt-box">
+									<button :data-id="dataItem.goods_id" @click="goodsDetail"
+										:style="'background:'+ pagestyleconfig.appstylecolor"
+										class="goodli-button">详情</button>
+								</view>
+							</view>
+						</block>
+					</view>
+				</view>
+				<view v-else class="diy-goods">
+					<view :class="'goods-list display__' + diyitem.base.display + ' column__' + diyitem.base.column">
+						<scroll-view :scroll-x="diyitem.base.display === 'slide' ? true : false">
+							<block v-for="(dataItem, index) in diyitem.list" :key="index">
+								<view v-if="dataItem.goods_id" class="goods-item">
+									<navigator hover-class="none"
+										:url="dataItem.goods_id > 0 ? '/pages/goodsDetail/goodsDetail?id=' + dataItem.goods_id : ''">
+										<view class="goods-image">
+											<image :style="diyitem.base.widthheight ? diyitem.base.widthheight : ''"
+												:src="dataItem.image"></image>
+										</view>
+										<view class="detail">
+											<view v-if="diyitem.base.text.show > 0"
+												class="goods-name twolist-hidden f-28">
+												{{ dataItem.goods_name }}
+											</view>
+											<view v-if="diyitem.base.sjg" class="goods-price col-m">
+												<text v-if="dataItem.is_points_goods == 1">{{lang.points}}:{{
+													dataItem.pay_points }}</text>
+												<text v-if="dataItem.is_points_goods != 1"><text class="f-24">¥</text>{{
+													dataItem.price }}
+													<block v-if="dataItem.ptype==2">
+														<text v-if="dataItem.time_amount > 0"
+															class="f-24">/{{dataItem.time_amount}}{{dataItem.quantity_unit}}</text>
+														<text v-else class="f-24">/<text
+																v-if="dataItem.is_times">{{dataItem.timesmum}}</text>次</text>
+													</block>
+												</text><text v-if="dataItem.original_price > 0"
+													class="tui-factory-price">¥{{
+														dataItem.original_price }}</text>
+												<text v-if="dataItem.minimum > 1"
+													style="color:#999;font-size:24rpx;">起售量
+													{{ dataItem.minimum }}</text>
+											</view>
+										</view>
+									</navigator>
+								</view>
+							</block>
+						</scroll-view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'goods',
+		props: {
+			diyitem: {
+				type: Object,
+				default () {
+					return {};
+				}
+			},
+			pagestyleconfig: {
+				type: Object,
+				default () {
+					return {};
+				}
+			}
+		},
+		computed: {
+
+		},
+		data() {
+			return {
+				lang: {}
+			};
+		},
+		mounted() {
+			let _this = this
+			_this.$request.get('Lang.getlang').then(res => {
+				if (res.errno == 0) {
+					_this.lang = res.data;
+				}
+			});
+		},
+		methods: {
+			navigateTo: function(e) {
+				this.sam.diynavigateTo(e)
+			},
+			goodsDetail: function(e) {
+				this.sam.navigateTo('/pages/goodsDetail/goodsDetail?id=' + e.currentTarget.dataset.id);
+			},
+			choosetechnical: function(e) {
+				this.sam.navigateTo('/pages/technical/list?goodsid=' + e.currentTarget.dataset.id);
+			},
+		}
+	};
+</script>
+<style>
+	@import './diyapge.css';
+</style>

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