Browse Source

完成登录页以及首页

wzy 1 year ago
parent
commit
af4b2c8bc3
100 changed files with 25847 additions and 10 deletions
  1. 16 0
      .hbuilderx/launch.json
  2. 9 8
      App.vue
  3. 87 0
      components/capsule/capsule.vue
  4. 187 0
      components/thorui/tui-actionsheet/tui-actionsheet.vue
  5. 135 0
      components/thorui/tui-alert/tui-alert.vue
  6. 203 0
      components/thorui/tui-alerts/tui-alerts.vue
  7. 156 0
      components/thorui/tui-badge/tui-badge.vue
  8. 386 0
      components/thorui/tui-bottom-navigation/tui-bottom-navigation.vue
  9. 108 0
      components/thorui/tui-bottom-popup/tui-bottom-popup.vue
  10. 204 0
      components/thorui/tui-bubble-popup/tui-bubble-popup.vue
  11. 627 0
      components/thorui/tui-button/tui-button.vue
  12. 562 0
      components/thorui/tui-calendar/tui-calendar.js
  13. 926 0
      components/thorui/tui-calendar/tui-calendar.vue
  14. 219 0
      components/thorui/tui-card/tui-card.vue
  15. 594 0
      components/thorui/tui-cascade-selection/tui-cascade-selection.vue
  16. 586 0
      components/thorui/tui-charts-column/tui-charts-column.vue
  17. 653 0
      components/thorui/tui-charts-line/tui-charts-line.vue
  18. 306 0
      components/thorui/tui-circular-progress/tui-circular-progress.vue
  19. 379 0
      components/thorui/tui-code-input/tui-code-input.vue
  20. 167 0
      components/thorui/tui-collapse/tui-collapse.vue
  21. 234 0
      components/thorui/tui-countdown-verify/tui-countdown-verify.vue
  22. 336 0
      components/thorui/tui-countdown/tui-countdown.vue
  23. 489 0
      components/thorui/tui-cropper/tui-cropper.vue
  24. 321 0
      components/thorui/tui-cropper/tui-cropper.wxs
  25. 655 0
      components/thorui/tui-datetime/tui-datetime.vue
  26. 193 0
      components/thorui/tui-dialog/tui-dialog.vue
  27. 103 0
      components/thorui/tui-divider/tui-divider.vue
  28. 140 0
      components/thorui/tui-drawer/tui-drawer.vue
  29. 69 0
      components/thorui/tui-dropdown-list/tui-dropdown-list.vue
  30. 276 0
      components/thorui/tui-fab/tui-fab.vue
  31. 118 0
      components/thorui/tui-footer/tui-footer.vue
  32. 277 0
      components/thorui/tui-form-button/tui-form-button.vue
  33. 29 0
      components/thorui/tui-form-field/tui-form-field.vue
  34. 202 0
      components/thorui/tui-form-item/tui-form-item.vue
  35. 200 0
      components/thorui/tui-form/tui-form.vue
  36. 313 0
      components/thorui/tui-form/tui-validation.js
  37. 204 0
      components/thorui/tui-gallery/tui-gallery.vue
  38. 153 0
      components/thorui/tui-grid-item/tui-grid-item.vue
  39. 44 0
      components/thorui/tui-grid/tui-grid.vue
  40. 190 0
      components/thorui/tui-icon/tui-icon.js
  41. 13 0
      components/thorui/tui-icon/tui-icon.vue
  42. 1091 0
      components/thorui/tui-image-cropper/tui-image-cropper.vue
  43. 164 0
      components/thorui/tui-image-group/tui-image-group.vue
  44. 621 0
      components/thorui/tui-index-list/tui-index-list.vue
  45. 578 0
      components/thorui/tui-input/tui-input.vue
  46. 73 0
      components/thorui/tui-keyboard-input/tui-keyboard-input.vue
  47. 241 0
      components/thorui/tui-keyboard/tui-keyboard.vue
  48. 54 0
      components/thorui/tui-label/tui-label.vue
  49. 158 0
      components/thorui/tui-landscape/tui-landscape.vue
  50. 223 0
      components/thorui/tui-lazyload-img/tui-lazyload-img.vue
  51. 178 0
      components/thorui/tui-list-cell/tui-list-cell.vue
  52. 97 0
      components/thorui/tui-list-view/tui-list-view.vue
  53. 78 0
      components/thorui/tui-loading/tui-loading.vue
  54. 161 0
      components/thorui/tui-loadmore/tui-loadmore.vue
  55. 446 0
      components/thorui/tui-modal/tui-modal.vue
  56. 249 0
      components/thorui/tui-navigation-bar/tui-navigation-bar.vue
  57. 118 0
      components/thorui/tui-no-data/tui-no-data.vue
  58. 115 0
      components/thorui/tui-nomore/tui-nomore.vue
  59. 240 0
      components/thorui/tui-numberbox/tui-numberbox.vue
  60. 210 0
      components/thorui/tui-pagination/tui-pagination.vue
  61. 502 0
      components/thorui/tui-picker/tui-picker.vue
  62. 675 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.vue
  63. 571 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.wxs
  64. 269 0
      components/thorui/tui-popup/tui-popup.vue
  65. 652 0
      components/thorui/tui-poster/tui-poster.vue
  66. 135 0
      components/thorui/tui-progress/tui-progress.vue
  67. 92 0
      components/thorui/tui-radio-group/tui-radio-group.vue
  68. 200 0
      components/thorui/tui-radio/tui-radio.vue
  69. 179 0
      components/thorui/tui-rate/tui-rate.vue
  70. 156 0
      components/thorui/tui-roll-news/tui-roll-news.vue
  71. 296 0
      components/thorui/tui-round-progress/tui-round-progress.vue
  72. 179 0
      components/thorui/tui-scroll-top/tui-scroll-top.vue
  73. 136 0
      components/thorui/tui-section/tui-section.vue
  74. 501 0
      components/thorui/tui-select/tui-select.vue
  75. 263 0
      components/thorui/tui-skeleton/tui-skeleton.vue
  76. 218 0
      components/thorui/tui-slide-verify/tui-slide-verify.vue
  77. 76 0
      components/thorui/tui-slide-verify/tui-slide-verify.wxs
  78. 255 0
      components/thorui/tui-steps/tui-steps.vue
  79. 125 0
      components/thorui/tui-sticky-wxs/tui-sticky-wxs.vue
  80. 47 0
      components/thorui/tui-sticky-wxs/tui-sticky.wxs
  81. 164 0
      components/thorui/tui-sticky/tui-sticky.vue
  82. 315 0
      components/thorui/tui-swipe-action/tui-swipe-action.vue
  83. 218 0
      components/thorui/tui-switch/tui-switch.vue
  84. 328 0
      components/thorui/tui-tab/tui-tab.vue
  85. 281 0
      components/thorui/tui-tabbar/tui-tabbar.vue
  86. 65 0
      components/thorui/tui-table/tui-table.vue
  87. 319 0
      components/thorui/tui-tabs/tui-tabs.vue
  88. 355 0
      components/thorui/tui-tag/tui-tag.vue
  89. 251 0
      components/thorui/tui-td/tui-td.vue
  90. 568 0
      components/thorui/tui-textarea/tui-textarea.vue
  91. 38 0
      components/thorui/tui-time-axis/tui-time-axis.vue
  92. 50 0
      components/thorui/tui-timeaxis-item/tui-timeaxis-item.vue
  93. 129 0
      components/thorui/tui-tips/tui-tips.vue
  94. 121 0
      components/thorui/tui-toast/tui-toast.vue
  95. 105 0
      components/thorui/tui-top-dropdown/tui-top-dropdown.vue
  96. 122 0
      components/thorui/tui-tr/tui-tr.vue
  97. 500 0
      components/thorui/tui-upload/tui-upload.vue
  98. 225 0
      components/thorui/tui-waterfall/tui-waterfall.vue
  99. 1 1
      index.html
  100. 1 1
      manifest.json

+ 16 - 0
.hbuilderx/launch.json

@@ -0,0 +1,16 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"mp-weixin" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 9 - 8
App.vue

@@ -1,17 +1,18 @@
+
 <script>
 	export default {
 		onLaunch: function() {
-			console.log('App Launch')
-		},
-		onShow: function() {
-			console.log('App Show')
+			console.log('App Launch',"一定会执行")
 		},
-		onHide: function() {
-			console.log('App Hide')
-		}
+		// onShow: function() {
+		// 	console.log('App Show')
+		// },
+		// onHide: function() {
+		// 	console.log('App Hide')
+		// }
 	}
 </script>
 
 <style>
-	/*每个页面公共css */
+	/* 每个页面公共css */
 </style>

+ 87 - 0
components/capsule/capsule.vue

@@ -0,0 +1,87 @@
+<template>
+    <view class="capsule" :style="{ backgroundColor: bgColor, paddingTop: top  }">
+        <view class="title" v-if="showBorder" :style="{ height: height, paddingLeft: titleLeft, display: isSecurity ? 'none' : 'block' }"
+            @click="handleClick">
+            <view class="icon" v-if="showIcon">
+                <tui-icon name="arrowleft" :size="28" color="#000"></tui-icon>
+            </view>
+            <text>{{ title }}</text>
+        </view>
+    </view>
+</template>
+
+<script>
+export default {
+    created() {
+        // #ifdef MP-WEIXIN
+        //  获取胶囊按钮的位置
+        let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
+        console.log(menuButtonInfo);
+        this.height = menuButtonInfo.height + 'px'
+        this.top = menuButtonInfo.top + 'px'
+        //  获取整个页面的宽度
+        uni.getSystemInfo({
+            success: res => {
+                console.log(res);
+                //  如果只需要安全区域的话 那就不需要高度
+                if (this.isSecurity) {
+                    this.top = res.safeAreaInsets.bottom + 'px'
+                    return
+                }
+                this.titleLeft = res.windowWidth - menuButtonInfo.right + "px"
+            }
+        })
+        // #endif
+    },
+    props: {
+        title: {
+            type: String,
+        },
+        bgColor: {
+            type: String,
+        },
+        //  是不是只需要安全区域
+        isSecurity: {
+            type: Boolean,
+            default: false
+        },
+        // 是否需要安全区域且于胶囊按钮平齐
+        showBorder: {
+            type: Boolean,
+            default: false
+        },
+        showIcon: {
+            type: Boolean,
+            default: false
+        },
+    },
+    data() {
+        return {
+            height: null,
+            top: null,
+            titleLeft: null,
+        }
+    },
+    methods: {
+        handleClick() {
+            this.$emit('click')
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.capsule {
+    width: 100%;
+    .title {
+        padding-left: 30rpx;
+        box-sizing: border-box;
+        color: rgba(0, 0, 0, 0.8);
+        font-size: 32rpx;
+        font-weight: 700;
+        display: flex !important;
+        align-items: center;
+        gap: 10rpx;
+    }
+}
+</style>

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

@@ -0,0 +1,187 @@
+<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",
+		emits: ['click','cancel'],
+		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 index = Number(e.currentTarget.dataset.index);
+				this.$emit('click', {
+					index: index,
+					...this.itemList[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>

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

@@ -0,0 +1,135 @@
+<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",
+		emits: ['click','cancel'],
+		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>

+ 203 - 0
components/thorui/tui-alerts/tui-alerts.vue

@@ -0,0 +1,203 @@
+<template>
+	<view class="tui-alert__wrap"
+		:style="{backgroundColor:backgroundColor?backgroundColor:getColor(type),borderRadius:radius,paddingTop:padding[0] || 0,paddingRight:padding[1]||0,paddingBottom:padding[2] || padding[0]||0,paddingLeft:padding[3] || padding[1]||0}">
+		<view class="tui-alert__shrink" @tap.stop="leftClick">
+			<slot name="left"></slot>
+			<icon :type="type" :size="iconSize" :color="iconColor" v-if="!isLeft"></icon>
+		</view>
+		<view class="tui-alert__content" :class="{'tui-text__p-left':!isLeft,'tui-text__p-right':closable}"
+			@tap.stop="onClick">
+			<text class="tui-alert__text" :style="{fontSize:size,color:color}" v-if="title">{{title}}</text>
+			<text class="tui-alert__text tui-desc__padding" :class="{'tui-alert__single':single}"
+				:style="{fontSize:descSize,color:descColor}" v-if="desc">{{desc}}</text>
+			<slot name="content"></slot>
+		</view>
+		<view class="tui-alert__shrink">
+			<slot name="right"></slot>
+		</view>
+		<icon @tap.stop="close" type="cancel" :size="closeSize" :color="closeColor" v-if="closable"
+			:class="{'tui-alert__icon-close':desc}">
+		</icon>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-alerts",
+		emits: ['leftClick','click','close'],
+		props: {
+			//info, success, warn, waiting,clear
+			type: {
+				type: String,
+				default: 'info'
+			},
+			//背景色,如果设置type对应颜色失效
+			backgroundColor: {
+				type: String,
+				default: ''
+			},
+			//nvue不支持简写,['20rpx','30rpx','20rpx','30rpx']=>[上,右,下,左]
+			padding: {
+				type: Array,
+				default () {
+					return ['20rpx', '30rpx']
+				}
+			},
+			radius: {
+				type: String,
+				default: '6rpx'
+			},
+			iconColor: {
+				type: String,
+				default: '#fff'
+			},
+			//icon字体大小,px
+			iconSize: {
+				type: Number,
+				default: 24
+			},
+			closable: {
+				type: Boolean,
+				default: false
+			},
+			closeColor: {
+				type: String,
+				default: '#fff'
+			},
+			//关闭icon字体大小,px
+			closeSize: {
+				type: Number,
+				default: 24
+			},
+			//是否自定义左侧内容
+			isLeft: {
+				type: Boolean,
+				default: false
+			},
+			isRight:{
+				type: Boolean,
+				default: false
+			},
+			title: {
+				type: String,
+				default: ''
+			},
+			color: {
+				type: String,
+				default: '#fff'
+			},
+			size: {
+				type: String,
+				default: '14px'
+			},
+			desc: {
+				type: String,
+				default: ''
+			},
+			descColor: {
+				type: String,
+				default: '#fff'
+			},
+			descSize: {
+				type: String,
+				default: '12px'
+			},
+			//描述文字单行展示,超出隐藏
+			single: {
+				type: Boolean,
+				default: false
+			}
+		},
+		methods: {
+			getColor(type) {
+				const color = "#5677fc"
+				const colors = {
+					'success': '#19be6b',
+					'warn': '#ff7900',
+					'clear': '#EB0909'
+				}
+				return colors[type] ? colors[type] : color;
+			},
+			leftClick() {
+				this.$emit('leftClick', {})
+			},
+			onClick() {
+				this.$emit('click', {})
+			},
+			close() {
+				this.$emit('close', {})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-alert__wrap {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		position: relative;
+	}
+
+	.tui-alert__shrink {
+		/* #ifndef APP-NVUE */
+		flex-shrink: 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		/* #endif */
+	}
+
+	.tui-alert__content {
+		flex: 1;
+		flex-direction: column;
+		overflow: hidden;
+	}
+
+	.tui-alert__text {
+		/* #ifndef APP-NVUE */
+		word-break: break-all;
+		display: block;
+		box-sizing: border-box;
+		/* #endif */
+	}
+	.tui-desc__padding{
+		padding-top: 3px;
+	}
+
+	.tui-text__p-left {
+		padding-left: 20rpx;
+	}
+
+	.tui-text__p-right {
+		padding-right: 60rpx;
+	}
+
+	.tui-alert__single {
+		/* #ifdef APP-NVUE */
+		lines: 1;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: block;
+		width: 100%;
+		white-space: nowrap;
+		/* #endif */
+		flex-direction: row;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	.tui-alert__icon-close {
+		position: absolute;
+		right: 30rpx;
+		top: 16rpx;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+</style>

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

@@ -0,0 +1,156 @@
+<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',
+		emits: ['click'],
+		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>

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

@@ -0,0 +1,386 @@
+<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="getIcon(current,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',
+	emits: ['click'],
+	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
+		}
+	},
+	data() {
+		return {
+			showMenuIndex: -1 //显示的菜单index
+		};
+	},
+	methods: {
+		getIcon: function(current, index, item) {
+			let url = item.iconPath;
+			if (item.type == 1) {
+				url = current == index ? item.selectedIconPath || item.iconPath : item.iconPath;
+			}
+			return url;
+		},
+		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: 1px 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: 1px 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>

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

@@ -0,0 +1,108 @@
+<template>
+	<view @touchmove.stop.prevent>
+		<view class="tui-popup-class tui-bottom-popup" :class="{ 'tui-popup-show': show, 'tui-popup-radius': radius }" :style="{ background: 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',
+		emits: ['close'],
+		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;
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+
+	.tui-popup-radius {
+		border-top-left-radius: 24rpx;
+		border-top-right-radius: 24rpx;
+		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>

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

@@ -0,0 +1,204 @@
+<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></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',
+		emits: ['close'],
+		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>

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

@@ -0,0 +1,627 @@
+<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' : '',
+			width==='100%' || !width || width===true?'tui-btn__flex-1':''
+		]" :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.stop="handleClick">
+		<slot></slot>
+	</button>
+</template>
+
+<script>
+	export default {
+		name: 'tuiButton',
+		emits: ['click', 'getuserinfo', 'contact', 'getphonenumber', 'error'],
+		// #ifndef VUE3
+		// #ifndef MP-QQ
+		behaviors: ['wx://form-field-button'],
+		// #endif
+		// #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;
+		/* #ifdef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+	}
+
+	.tui-gray-primary-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-green {
+		background: #f2f2f2 !important;
+		color: #07c160 !important;
+		/* #ifdef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+	}
+
+	.tui-gray-green-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-danger {
+		background: #f2f2f2 !important;
+		color: #eb0909 !important;
+		/* #ifdef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+	}
+
+	.tui-gray-danger-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-warning {
+		background: #f2f2f2 !important;
+		color: #fc872d !important;
+		/* #ifdef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+	}
+
+	.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;
+		/* #ifndef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+		/* #ifdef MP-QQ */
+		border: 1rpx solid;
+		box-sizing: border-box;
+		/* #endif */
+		border-radius: 6rpx;
+		padding-left: 0;
+		padding-right: 0;
+		overflow: visible;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+	}
+
+	.tui-btn__flex-1 {
+		flex: 1;
+	}
+
+	/* #ifndef MP-QQ */
+	.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;
+	}
+
+	/* #endif */
+
+
+	/* #ifdef MP-BAIDU */
+	/* .tui-btn:active::after {
+		background-color: rgba(0, 0, 0, 0.2) !important;
+	} */
+	/* #endif */
+
+	.tui-text-bold {
+		font-weight: bold;
+	}
+
+	/* #ifdef MP-QQ */
+	.tui-btn-white {
+		border: 1rpx solid #bfbfbf;
+	}
+
+	/* #endif */
+
+	/* #ifndef MP-QQ */
+	.tui-btn-white::after {
+		border: 1px solid #bfbfbf;
+	}
+
+	/* #endif */
+
+
+	.tui-white-hover {
+		background: #e5e5e5 !important;
+		color: #2e2e2e !important;
+	}
+
+	.tui-dark-disabled {
+		opacity: 0.6 !important;
+		color: #fafbfc !important;
+	}
+
+	.tui-dark-disabled-outline {
+		/* #ifdef MP-QQ */
+		border: 0 !important;
+		/* #endif */
+		opacity: 0.5 !important;
+	}
+
+	.tui-gray-disabled {
+		background: #f3f3f3 !important;
+		color: #919191 !important;
+		box-shadow: none;
+	}
+
+	.tui-outline-hover {
+		opacity: 0.5 !important;
+	}
+
+	.tui-primary-hover {
+		background: #4a67d6 !important;
+		color: #e5e5e5 !important;
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-primary-outline::after {
+		border: 1px solid #5677fc !important;
+	}
+
+	/* #endif */
+
+	.tui-primary-outline {
+		color: #5677fc !important;
+		background: transparent;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #5677fc !important;
+		/* #endif */
+	}
+
+	.tui-danger-hover {
+		background: #c80808 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-danger-outline {
+		color: #eb0909 !important;
+		background: transparent;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #eb0909 !important;
+		/* #endif */
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-danger-outline::after {
+		border: 1px solid #eb0909 !important;
+	}
+
+	/* #endif */
+
+	.tui-warning-hover {
+		background: #d67326 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-warning-outline {
+		color: #fc872d !important;
+		background: transparent;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #fc872d !important;
+		/* #endif */
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-warning-outline::after {
+		border: 1px solid #fc872d !important;
+	}
+
+	/* #endif */
+
+	.tui-green-hover {
+		background: #06ad56 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-green-outline {
+		color: #07c160 !important;
+		background: transparent;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #07c160 !important;
+		/* #endif */
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-green-outline::after {
+		border: 1px solid #07c160 !important;
+	}
+
+	/* #endif */
+
+	.tui-blue-hover {
+		background: #0062cc !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-blue-outline {
+		color: #007aff !important;
+		background: transparent;
+		/* #ifdef APP-PLUS */
+		border: 1rpx solid #007aff !important;
+		/* #endif */
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-blue-outline::after {
+		border: 1px solid #007aff !important;
+	}
+
+	/* #endif */
+
+	/* #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;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #fff !important;
+		/* #endif */
+	}
+
+	.tui-black-outline {
+		background: transparent !important;
+		color: #333 !important;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #ccc !important;
+		/* #endif */
+	}
+
+	/* #ifndef MP-QQ */
+	.tui-gray-outline::after {
+		border: 1px solid #ccc !important;
+	}
+
+	/* #endif */
+
+	/* #ifndef MP-QQ */
+	.tui-white-outline::after {
+		border: 1px solid #fff !important;
+	}
+
+	.tui-black-outline::after {
+		border: 1px solid #333 !important;
+	}
+
+	.tui-brown-outline::after {
+		border: 1px solid #ac9157 !important;
+	}
+
+	/* #endif */
+
+	.tui-brown-outline {
+		color: #ac9157 !important;
+		background: transparent;
+		/* #ifdef MP-QQ */
+		border: 1rpx solid #ac9157 !important;
+		/* #endif */
+	}
+
+	/*圆角 */
+
+	.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;
+	}
+
+	/* #ifdef MP-QQ */
+	.tui-btn__link {
+		border: 0 !important;
+	}
+
+	/* #endif */
+
+	.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);
+	}
+};
+
+export default {
+	solar2lunar: calendar.solar2lunar,
+	lunar2solar: calendar.lunar2solar
+};

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

@@ -0,0 +1,926 @@
+<template>
+	<view @touchmove.stop.prevent="stop" v-if="isFixed">
+		<view class="tui-bottom-popup" :class="{'tui-popup-show': isShow}">
+			<view class="tui-calendar-header" :class="{ 'tui-calendar-radius': radius }">
+				<view>{{title}}</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">
+				<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" :disabled="disabled"
+						@click="btnFix(false)">确定
+					</tui-button>
+				</view>
+			</view>
+		</view>
+
+		<view class="tui-popup-mask" :class="[isShow ? 'tui-mask-show' : '']" @tap="hide"></view>
+	</view>
+	<view v-else>
+		<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" :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>
+</template>
+<script>
+	//easycom组件模式 无需手动引入
+	// import tuiButton from "../tui-button/tui-button.vue"
+	import calendar from './tui-calendar.js';
+	export default {
+		name: 'tuiCalendar',
+		emits: ['hide', 'change'],
+		// components:{
+		// 	tuiButton
+		// },
+		props: {
+			//1-切换月份和年份 2-切换月份
+			arrowType: {
+				type: [Number, String],
+				default: 1
+			},
+			//1-单个日期选择 2-开始日期+结束日期选择 3-多个日期
+			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: ''
+			},
+			title: {
+				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, Array],
+				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}`;
+			},
+			disabled() {
+				return this.type == 2 && (!this.startDate || !this.endDate)
+			}
+		},
+		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);
+				if (this.openDisAbled(this.year, this.month, this.day)) {
+					this.year = this.min.year;
+					this.month = this.min.month;
+					this.day = this.min.day;
+					this.activeDate = `${this.min.year}-${this.min.month}-${this.min.day}`;
+					this.max = this.initDate(this.maxDate || this.minDate);
+				}
+				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 false;
+			},
+			//一个月有多少天
+			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>

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

@@ -0,0 +1,219 @@
+<template>
+	<view class="tui-card-class tui-card" :class="[full?'tui-card-full':'',border?'tui-card-border':'']"
+		@tap="handleClick" @longtap="longTap">
+		<slot>
+			<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>
+		</slot>
+		<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",
+		emits: ['click', 'longclick'],
+		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>

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

@@ -0,0 +1,594 @@
+<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': idx === currentTab && bold }"
+					:style="{ color: idx === currentTab ? activeColor : color, fontSize: size + 'rpx' }"
+					:id="`id_${idx}`" @tap.stop="swichNav" :data-current="idx" v-for="(item, idx) in selectedArr"
+					:key="idx">
+					{{ item.text }}
+					<view class="tui-active-line" :style="{ backgroundColor: lineColor }"
+						v-if="idx === currentTab && showLine"></view>
+				</view>
+			</view>
+		</scroll-view>
+		<swiper class="tui-selection-list" :current="defTab" 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.stop="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',
+		emits: ['change', 'complete'],
+		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,
+				default(){
+					return []
+				}
+			},
+			defaultKey: {
+				type: String,
+				default: 'text'
+			},
+			//是否显示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,
+				defTab: 0,
+				//tab栏scrollview滚动的位置
+				scrollViewId: 'id__1',
+				selectedArr: []
+			};
+		},
+		methods: {
+			setDefaultData(val) {
+				let defaultItemList = JSON.parse(JSON.stringify(val || []));
+				if (defaultItemList.length > 0) {
+					if ((typeof defaultItemList[0] === 'string' || typeof defaultItemList[0] === 'number') && !this
+						.request) {
+						let subi = -1
+						let selectedArr = []
+						for (let j = 0, len = defaultItemList.length; j < len; j++) {
+							let item = defaultItemList[j]
+							let list = []
+							let obj = {}
+							if (j === 0) {
+								list = this.getItemList(-1)
+							} else {
+								list = this.getItemList(j - 1, subi, selectedArr)
+							}
+							subi = this.getDefaultIndex(list, item)
+							if (subi !== -1) {
+								obj = list[subi]
+								selectedArr.push({
+									text: obj.text || this.text,
+									value: obj.value || '',
+									src: obj.src || '',
+									subText: obj.subText || '',
+									index: subi,
+									scrollViewId: `id_${subi}`,
+									list: list
+								})
+							}
+
+							if (subi === -1) break;
+						}
+						this.selectedArr = selectedArr;
+						this.defTab = this.currentTab;
+						this.$nextTick(() => {
+							setTimeout(() => {
+								this.currentTab = selectedArr.length - 1;
+								this.defTab = this.currentTab;
+								this.checkCor();
+							}, 20)
+						});
+					} else {
+						defaultItemList.map(item => {
+							item.scrollViewId = `id_${item.index}`;
+						});
+						this.selectedArr = defaultItemList;
+						this.defTab = this.currentTab;
+						this.$nextTick(() => {
+							setTimeout(() => {
+								this.currentTab = defaultItemList.length - 1;
+								this.defTab = this.currentTab;
+								this.checkCor();
+							}, 20)
+						});
+					}
+
+				} else {
+					this.initData(this.itemList, -1);
+				}
+			},
+			getDefaultIndex(arr, val) {
+				if (!arr || arr.length === 0 || val === undefined) return -1;
+				let index = -1;
+				let key = this.defaultKey || 'text'
+				for (let i = 0, len = arr.length; i < len; i++) {
+					if (arr[i][key] == val) {
+						index = i;
+						break;
+					}
+				}
+				return index;
+			},
+			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, selectedArr) {
+				let list = [];
+				let arr = JSON.parse(JSON.stringify(this.itemList));
+				selectedArr = selectedArr || this.selectedArr
+				if (layer == -1) {
+					list = this.removeChildren(arr);
+				} else {
+					let value = selectedArr[0].index;
+					value = value === undefined || value == -1 ? index : value;
+					if (arr[value] && arr[value].children) {
+						list = arr[value].children;
+					}
+					if (layer > 0) {
+						for (let i = 1; i < layer + 1; i++) {
+							let val = layer === i ? index : selectedArr[i].index;
+							list = val === -1 ? [] : (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.defTab = this.currentTab;
+					setTimeout(() => {
+						this.currentTab = cur;
+						this.defTab = this.currentTab;
+					}, 20)
+				}
+			},
+			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}`;
+					}, 20);
+				});
+
+				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 arr = this.selectedArr;
+					if (layer < arr.length - 1) {
+						let newArr = arr.slice(0, layer + 1);
+						this.selectedArr = newArr;
+					}
+					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);
+					}
+
+					let current = this.selectedArr.length - 1;
+					if (current >= this.currentTab) {
+						this.defTab = this.currentTab
+					}
+					this.$nextTick(() => {
+						setTimeout(() => {
+							this.defTab = current;
+							this.currentTab = current;
+							this.scrollViewId = `id_${this.currentTab > 1?this.currentTab - 1:0}`;
+						}, 50)
+
+					});
+				}
+			}
+		}
+	};
+</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>

+ 586 - 0
components/thorui/tui-charts-column/tui-charts-column.vue

@@ -0,0 +1,586 @@
+<template>
+	<view class="tui-charts__column-wrap">
+		<view class="tui-column__legend" v-if="legend.show">
+			<view class="tui-column__legend-item" v-for="(item,index) in dataset" :key="index">
+				<view class="tui-legend__circle" :style="{backgroundColor:item.color}"></view>
+				<text
+					:style="{fontSize:(legend.size || 24)+'rpx',lineHeight:(legend.size || 24)+'rpx',color:legend.color || '#333'}">{{item.name}}</text>
+			</view>
+		</view>
+		<view class="tui-charts__column-box" v-if="xAxis.length>0 && dataset.length>0">
+			<scroll-view :scroll-x="scrollable" class="tui-column__scroll-view" :style="{height:scrollViewH+'rpx'}">
+				<view :style="{height:(xAxisVal.height || 2) +'rpx'}" v-if="xAxisVal.show"></view>
+				<view class="tui-charts__column" :style="{height:height+'rpx'}">
+					<view class="tui-column__item"
+						:class="{'tui-column__flex-1':!scrollable,'tui-column__flex-column':isStack,'tui-column__item-active':activeIndex===index && clickEffect==1,'tui-column__bar-opacity':clickEffect==2,'tui-column__bar-active':clickEffect==2 && activeIndex==index}"
+						:style="{padding:scrollable? (xAxisLine.itemPadding ||'0 30rpx'):'0' }"
+						v-for="(item,index) in xAxis" :key="index">
+						<view class="tui-column__val"
+							v-if="(xAxisVal.show && clickEffect!=2 ) || (xAxisVal.show && clickEffect==2 && activeIndex===index)"
+							:style="{color:xAxisVal.color,fontSize:(xAxisVal.size || 24)+'rpx',whiteSpace: xAxisVal.nowrap?'nowrap':'normal'}">
+							{{getYAxisVal(index)}}
+						</view>
+						<view class="tui-column__bar" :class="{'tui-column__bar-round':columnCap==='round'}"
+							v-for="(bar,idx) in dataset" :key="idx"
+							:style="{width:columnBarWidth+'rpx',borderTopColor:getBarColor(bar.source[index],bar.color,bar.colorFormatter),background:getBarColor(bar.source[index],bar.color,bar.colorFormatter),height:((bar.source[index]-(isStack?(min/dataset.length):min))/splitNumber)*(yAxisLine.itemGap || 60) +'rpx'}"
+							@tap.stop="onBarTap(index,idx)">
+						</view>
+						<view class="tui-column__xAxis-text"
+							:style="{color:xAxisLabel.color || '#333',fontSize:(xAxisLabel.size || 24)+'rpx' }">
+							{{item}}
+						</view>
+						<view class="tui-xAxis__tickmarks"
+							:style="{height:xAxisTick.height || '12rpx',backgroundColor:xAxisTick.color || '#e3e3e3'}">
+						</view>
+					</view>
+				</view>
+			</scroll-view>
+			<view class="tui-column__border-left"
+				:style="{height:height+(xAxisVal.show?(xAxisVal.height || 2):0)+'rpx',backgroundColor:yAxisLine.color || '#e3e3e3'}">
+			</view>
+			<view class="tui-xAxis__line" :class="{'tui-line__first':index===0}"
+				:style="{bottom:index*(yAxisLine.itemGap || 60)+(xAxisLabel.height || 60)+'rpx',borderTopStyle:index===0?'solid':splitLine.type,borderTopColor:index===0?xAxisLine.color:splitLine.color}"
+				v-for="(item,index) in yAxisData" :key="index">
+				<text class="tui-yAxis__val"
+					:style="{color:item.color || yAxisLabel.color,fontSize:(yAxisLabel.size||24)+'rpx'}"
+					v-if="yAxisLabel.show">{{item.value}}</text>
+			</view>
+		</view>
+		<view class="tui-column__tooltip" v-if="tooltip" :class="{'tui-column__tooltip-show':tooltipShow}">
+			<view class="tui-tooltip__title">{{xAxis[activeIndex] || ''}}</view>
+			<view class="tui-column__tooltip-item" v-for="(item,index) in tooltips" :key="index">
+				<view class="tui-legend__circle" :style="{backgroundColor:item.color}"></view>
+				<text class="tui-tooltip__val">{{item.name}}</text>
+				<text class="tui-tooltip__val tui-tooltip__val-ml">{{item.val}}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-charts-column",
+		emits: ['click'],
+		props: {
+			//图例,说明
+			legend: {
+				type: Object,
+				default () {
+					return {
+						show: false,
+						size: 24,
+						color: '#333'
+					}
+				}
+			},
+			tooltip: {
+				type: Boolean,
+				default: false
+			},
+			xAxis: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//默认选中x轴索引
+			currentIndex: {
+				type: Number,
+				default: -1
+			},
+			//柱状条宽度
+			columnBarWidth: {
+				type: [Number, String],
+				default: 32
+			},
+			//分割线
+			splitLine: {
+				type: Object,
+				default () {
+					return {
+						//分割线颜色,不显示则将颜色设置为transparent
+						color: "#e3e3e3",
+						type: "dashed"
+					}
+				}
+			},
+			//x轴刻度线
+			xAxisTick: {
+				type: Object,
+				default () {
+					return {
+						height: '12rpx',
+						//不显示则将颜色设置为transparent
+						color: '#e3e3e3'
+					}
+				}
+			},
+			//x轴线条
+			xAxisLine: {
+				type: Object,
+				default () {
+					return {
+						color: '#e3e3e3',
+						//x轴item的padding值
+						itemPadding: '0 30rpx'
+					}
+				}
+			},
+			xAxisLabel: {
+				type: Object,
+				default () {
+					return {
+						color: "#333",
+						size: 24,
+						height: 60
+					}
+				}
+			},
+			xAxisVal: {
+				type: Object,
+				default () {
+					return {
+						show: false,
+						color: "#333",
+						size: 24,
+						//如果show为true且val显示的时候,height需要设置一定的值保证val能显示完整 rpx
+						height: 60
+					}
+				}
+			},
+			//y轴数据,如果不传则默认使用min,max值计算
+			// {
+			// 	value: 0,
+			// 	color: "#333"
+			// }
+			yAxis: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//y轴最小值
+			min: {
+				type: Number,
+				default: 0
+			},
+			//y轴最大值
+			max: {
+				type: Number,
+				default: 100
+			},
+			//y轴分段递增数值 
+			splitNumber: {
+				type: Number,
+				default: 20
+			},
+			yAxisLine: {
+				type: Object,
+				default () {
+					return {
+						//不显示则将颜色设置为transparent
+						color: '#e3e3e3',
+						//y轴item间距 rpx
+						itemGap: 60
+					}
+				}
+			},
+			yAxisLabel: {
+				type: Object,
+				default () {
+					return {
+						show: true,
+						color: "#333",
+						size: 24
+					}
+				}
+			},
+			//是否可滚动
+			scrollable: {
+				type: Boolean,
+				default: false
+			},
+			//是否堆叠展示
+			isStack: {
+				type: Boolean,
+				default: false
+			},
+			//柱状条点击效果:1-出现背景,2-高亮显示,其他变暗 3-无效果
+			clickEffect: {
+				type: Number,
+				default: 1
+			},
+			/*
+				 柱状条的端点样式
+				 round	向线条的每个末端添加圆形线帽
+				 square	向线条的每个末端添加正方形线帽
+				*/
+			columnCap: {
+				type: String,
+				default: 'square'
+			}
+		},
+		data() {
+			return {
+				height: 0,
+				scrollViewH: 0,
+				sections: 0,
+				yAxisData: [],
+				activeIndex: -1,
+				activeIdx: -1,
+				tooltips: [],
+				tooltipShow: false,
+				timer: null,
+				/*========options============*/
+				/*
+					name: '', 
+					color: '',
+					source: []
+					colorFormatter:Function
+				*/
+				dataset: [],
+				xAxisValFormatter: null
+			}
+		},
+		watch: {
+			yAxis(newVal) {
+				this.init()
+			},
+			currentIndex(newVal) {
+				if (newVal != this.activeIndex) {
+					this.activeIndex = newVal
+				}
+			}
+		},
+		created() {
+			this.init()
+			this.activeIndex = this.currentIndex;
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.clearTimer()
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.clearTimer()
+		},
+		// #endif
+		methods: {
+			generateArray(start, end) {
+				return Array.from(new Array(end + 1).keys()).slice(start);
+			},
+			getYAxisVal(index) {
+				let showVal = '';
+				let val = 0;
+				if (this.dataset.length === 1) {
+					val = this.dataset[0].source[index]
+					showVal = val;
+				} else if (this.dataset.length > 1) {
+					let arr = []
+					this.dataset.forEach(item => {
+						arr.push(item.source[index])
+					})
+					val = arr
+				}
+				if (this.xAxisVal.formatter && typeof this.xAxisVal.formatter === 'function') {
+					showVal = this.xAxisVal.formatter(val)
+				} else if (this.xAxisValFormatter && typeof this.xAxisValFormatter === 'function') {
+					showVal = this.xAxisValFormatter(val)
+				}
+				return showVal
+			},
+			getBarColor(val, color, colorFormatter) {
+				let bgColor = color;
+				if (colorFormatter && typeof colorFormatter === 'function') {
+					let formatColor = colorFormatter(val)
+					if (formatColor) {
+						bgColor = formatColor
+					}
+				}
+				return bgColor
+			},
+			init() {
+				let itemGap = this.yAxisLine.itemGap || 60;
+				let sections = this.yAxis.length - 1;
+				let yAxis = this.yAxis;
+				if (sections <= 0) {
+					sections = Math.ceil((this.max - this.min) / this.splitNumber)
+					let sectionsArr = this.generateArray(0, sections)
+
+					yAxis = sectionsArr.map(item => {
+						return {
+							value: item * this.splitNumber + this.min
+						}
+					})
+				}
+				this.yAxisData = yAxis;
+				this.sections = sections + 1;
+				this.height = itemGap * sections;
+				const valH = this.xAxisVal.show ? (this.xAxisVal.height || 2) : 0;
+				this.scrollViewH = this.height + (this.xAxisLabel.height || 60) + valH;
+			},
+			clearTimer() {
+				clearTimeout(this.timer)
+				this.timer = null;
+			},
+			tooltipHandle(index) {
+				let data = [...this.dataset]
+				let tooltips = []
+				data.forEach(item => {
+					let color = item.color;
+					if (item.colorFormatter && typeof item.colorFormatter === 'function') {
+						color = item.colorFormatter(item.source[index])
+					}
+					tooltips.push({
+						color: color,
+						name: item.name,
+						val: item.source[index]
+					})
+				})
+				this.tooltips = tooltips;
+				this.clearTimer()
+				this.tooltipShow = true;
+				this.timer = setTimeout(() => {
+					this.tooltipShow = false
+				}, 5000)
+			},
+			onBarTap(index, idx) {
+				this.activeIndex = index;
+				this.activeIdx = idx;
+				this.tooltipHandle(index);
+				this.$emit('click', {
+					datasetIndex: idx,
+					sourceIndex: index,
+					...this.dataset[idx]
+				})
+			},
+			/*
+			dataset:柱状图表数据
+			xAxisValFormatter :格式化柱状条顶部value值(此处传值是为了做兼容处理)
+			*/
+			draw(dataset, xAxisValFormatter) {
+				this.xAxisValFormatter = xAxisValFormatter || null;
+				this.dataset = dataset || [];
+				this.init();
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-charts__column-wrap {
+		position: relative;
+		overflow: visible;
+	}
+
+	.tui-charts__column-box {
+		position: relative;
+		padding-left: 1px;
+		box-sizing: border-box;
+	}
+
+	.tui-column__scroll-view {
+		position: relative;
+		z-index: 10;
+		box-sizing: border-box;
+	}
+
+	.tui-charts__column {
+		width: 100%;
+		position: relative;
+		display: flex;
+		align-items: flex-end;
+		position: relative;
+	}
+
+	.tui-column__between {
+		justify-content: space-between;
+	}
+
+	.tui-column__item {
+		display: flex;
+		align-items: flex-end;
+		justify-content: center;
+		position: relative;
+		text-align: center;
+		box-sizing: border-box;
+		z-index: 10;
+		transition: all 0.3s;
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-column__bar-opacity {
+		opacity: 0.6;
+	}
+
+	.tui-column__bar-active {
+		/*这里请勿随意将值改为1*/
+		opacity: 0.9999;
+	}
+
+	.tui-column__flex-1 {
+		flex: 1;
+	}
+
+	.tui-column__item-active {
+		background-color: rgba(0, 0, 0, .1);
+		padding-top: 20rpx !important;
+	}
+
+	.tui-column__flex-column {
+		flex-direction: column;
+		justify-content: flex-end;
+		align-items: center;
+	}
+
+	.tui-xAxis__tickmarks {
+		position: absolute;
+		right: 0;
+		width: 1px;
+		transform: translateY(100%);
+		bottom: 0;
+	}
+
+	.tui-column__bar {
+		transition: all 0.3s;
+		flex-shrink: 0;
+		text-align: center;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+		/* border-top: 1px solid; */
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-column__bar-round {
+		border-top-left-radius: 100px;
+		border-top-right-radius: 100px;
+	}
+
+	.tui-column__val {
+		width: 100%;
+		position: absolute;
+		top: 0;
+		left: 50%;
+		padding-bottom: 12rpx;
+		transform: translate(-50%, -100%);
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+		word-break: break-all;
+	}
+
+	.tui-column__xAxis-text {
+		width: 100%;
+		position: absolute;
+		left: 50%;
+		bottom: 0;
+		flex: 1;
+		transform: translate(-50%, 100%);
+		padding-top: 8rpx;
+		word-break: break-all;
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-column__border-left {
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 1px;
+		z-index: 11;
+	}
+
+	.tui-xAxis__line {
+		width: 100%;
+		height: 0;
+		border-top-width: 1px;
+		position: absolute;
+		left: 0;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-line__first {
+		z-index: 12;
+	}
+
+	.tui-yAxis__val {
+		transform: translateX(-100%);
+		padding-right: 12rpx;
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-column__legend {
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+	}
+
+	.tui-column__legend-item {
+		display: flex;
+		align-items: center;
+		margin-left: 24rpx;
+		margin-bottom: 30rpx;
+	}
+
+	.tui-legend__circle {
+		height: 20rpx;
+		width: 20rpx;
+		border-radius: 50%;
+		margin-right: 8rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-column__tooltip {
+		padding: 30rpx;
+		border-radius: 12rpx;
+		background-color: rgba(0, 0, 0, .6);
+		display: inline-block;
+		position: absolute;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		z-index: 20;
+		visibility: hidden;
+		opacity: 0;
+		transition: all 0.3s;
+	}
+
+	.tui-column__tooltip-show {
+		visibility: visible;
+		opacity: 1;
+	}
+
+	.tui-tooltip__title {
+		font-size: 30rpx;
+		color: #fff;
+		line-height: 30rpx;
+	}
+
+	.tui-column__tooltip-item {
+		display: flex;
+		align-items: center;
+		padding-top: 24rpx;
+		white-space: nowrap;
+	}
+
+	.tui-tooltip__val {
+		font-size: 24rpx;
+		line-height: 24rpx;
+		color: #fff;
+		margin-left: 6rpx;
+	}
+
+	.tui-tooltip__val-ml {
+		margin-left: 20rpx;
+	}
+</style>

+ 653 - 0
components/thorui/tui-charts-line/tui-charts-line.vue

@@ -0,0 +1,653 @@
+<template>
+	<view class="tui-charts__line-wrap" :style="{width:width+'rpx'}">
+		<view class="tui-line__legend" v-if="legend.show">
+			<view class="tui-line__legend-item" v-for="(item,index) in dataset" :key="index">
+				<view class="tui-line__legend-circle" :style="{backgroundColor:item.color}"></view>
+				<text
+					:style="{fontSize:(legend.size || 24)+'rpx',lineHeight:(legend.size || 24)+'rpx',color:legend.color || '#333'}">{{item.name}}</text>
+			</view>
+		</view>
+		<view class="tui-charts__line-box" v-if="xAxis.length>0 && dataset.length>0" :style="{width:width+'rpx'}">
+			<scroll-view :scroll-x="scrollable" class="tui-line__scroll-view" :style="{height:scrollViewH+'rpx'}">
+				<view :style="{height:(xAxisVal.height || 48) +'rpx'}"></view>
+				<view class="tui-charts__line" :style="{height:height+'rpx'}">
+					<view class="tui-line__item" :class="{'tui-line__flex-1':!scrollable}"
+						:style="{width:(xAxisLine.itemGap || 120)+'rpx'}" v-for="(item,index) in xAxis" :key="index">
+						<view class="tui-line__xAxis-text"
+							:style="{color:xAxisLabel.color || '#333',fontSize:(xAxisLabel.size || 24)+'rpx' }">
+							{{item}}
+						</view>
+						<view class="tui-yAxis__split-line"
+							:style="{borderRightStyle:yAxisSplitLine.type || 'dashed',borderRightColor:yAxisSplitLine.color || '#e3e3e3'}"
+							v-if="tooltipShow && index==activeIdx">
+						</view>
+						<view class="tui-xAxis__tickmarks"
+							:style="{height:xAxisTick.height || '12rpx',backgroundColor:xAxisTick.color || '#e3e3e3'}">
+						</view>
+					</view>
+					<view v-for="(dot,i) in dots" :key="dot.id">
+						<view class="tui-charts__line-dot"
+							:class="{'tui-charts__dot-enlarge':tooltipShow && j==activeIdx}" @tap.stop="dotClick(i,j)"
+							v-for="(d,j) in dot.source" :key="d.id"
+							:style="{bottom: d.y+'rpx', left: d.x+'rpx',width:(brokenDot.width || 12)+'rpx',height:(brokenDot.width || 12)+'rpx',borderColor:dot.color || brokenDot.color,background:brokenDot.color || dot.color}">
+							<text class="tui-line__val"
+								:style="{fontSize:(xAxisVal.size || 24)+'rpx',color:xAxisVal.color}"
+								v-if="xAxisVal.show">
+								{{getYAxisVal(i,j)}}
+							</text>
+						</view>
+					</view>
+
+					<view v-for="(line,idx) in lines" :key="line.id">
+						<view class="tui-charts__broken-line" v-for="(l,k) in line.source" :key="l.id"
+							:style="{height:brokenLineHeight+'px',background:line.color,bottom: l.y+'rpx', left: l.x+'rpx',width: l.width+'rpx','-webkit-transform': `rotate(${l.angle}deg)`,transform: `rotate(${l.angle}deg)`}">
+						</view>
+					</view>
+				</view>
+			</scroll-view>
+			<view class="tui-line__border-left"
+				:style="{height:height+(xAxisVal.height || 48)+'rpx',backgroundColor:yAxisLine.color || '#e3e3e3'}">
+			</view>
+			<view class="tui-xAxis__line" :class="{'tui-line__first':index===0}"
+				:style="{bottom:index*(yAxisLine.itemGap || 60)+(xAxisLabel.height || 60)+'rpx',borderTopStyle:index===0?'solid':splitLine.type,borderTopColor:index===0?xAxisLine.color:splitLine.color}"
+				v-for="(item,index) in yAxisData" :key="index">
+				<text class="tui-yAxis__val"
+					:style="{color:item.color || yAxisLabel.color,fontSize:(yAxisLabel.size||24)+'rpx'}"
+					v-if="yAxisLabel.show">{{item.value}}</text>
+			</view>
+		</view>
+
+		<view class="tui-line__tooltip" v-if="tooltip" :class="{'tui-line__tooltip-show':tooltipShow}">
+			<view class="tui-tooltip__title">{{xAxis[activeIdx] || ''}}</view>
+			<view class="tui-line__tooltip-item" v-for="(item,index) in tooltips" :key="index">
+				<view class="tui-line__legend-circle" :style="{backgroundColor:item.color}"></view>
+				<text class="tui-tooltip__val">{{item.name}}</text>
+				<text class="tui-tooltip__val tui-tooltip__val-ml">{{item.val}}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-charts-line",
+		emits: ['click'],
+		props: {
+			//图表宽度
+			width: {
+				type: [Number, String],
+				default: 620
+			},
+			//图例,说明
+			legend: {
+				type: Object,
+				default () {
+					return {
+						show: false,
+						size: 24,
+						color: '#333'
+					}
+				}
+			},
+			tooltip: {
+				type: Boolean,
+				default: false
+			},
+			xAxis: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//默认选中x轴索引
+			currentIndex: {
+				type: Number,
+				default: -1
+			},
+			//分割线
+			splitLine: {
+				type: Object,
+				default () {
+					return {
+						//分割线颜色,不显示则将颜色设置为transparent
+						color: "#e3e3e3",
+						type: "dashed"
+					}
+				}
+			},
+			//x轴刻度线
+			xAxisTick: {
+				type: Object,
+				default () {
+					return {
+						height: '12rpx',
+						//不显示则将颜色设置为transparent
+						color: '#e3e3e3'
+					}
+				}
+			},
+			//x轴线条
+			xAxisLine: {
+				type: Object,
+				default () {
+					return {
+						color: '#e3e3e3',
+						//x轴item间距 rpx
+						itemGap: 120
+					}
+				}
+			},
+			xAxisLabel: {
+				type: Object,
+				default () {
+					return {
+						color: "#333",
+						size: 24,
+						height: 60
+					}
+				}
+			},
+			xAxisVal: {
+				type: Object,
+				default () {
+					return {
+						show: true,
+						color: "#333",
+						size: 24,
+						//如果show为true且val显示的时候,height需要设置一定的值保证val能显示完整 rpx
+						height: 48
+					}
+				}
+			},
+			//点击坐标点所显示的分割线
+			yAxisSplitLine: {
+				type: Object,
+				default () {
+					return {
+						//分割线颜色,不显示则将颜色设置为transparent
+						color: "transparent",
+						type: "dashed"
+					}
+				}
+			},
+			//折线坐标点宽度 rpx
+			brokenDot: {
+				type: Object,
+				default () {
+					return {
+						width: 12,
+						//点的背景色
+						color: '#F8F8F8'
+					}
+				}
+			},
+			//折线高度/粗细 px
+			brokenLineHeight: {
+				type: [Number, String],
+				default: 1
+			},
+			//y轴数据,如果不传则默认使用min,max值计算
+			// {
+			// 	value: 0,
+			// 	color: "#333"
+			// }
+			yAxis: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//y轴最小值
+			min: {
+				type: Number,
+				default: 0
+			},
+			//y轴最大值
+			max: {
+				type: Number,
+				default: 100
+			},
+			//y轴分段递增数值 
+			splitNumber: {
+				type: Number,
+				default: 20
+			},
+			yAxisLine: {
+				type: Object,
+				default () {
+					return {
+						//不显示则将颜色设置为transparent
+						color: '#e3e3e3',
+						//y轴item间距 rpx
+						itemGap: 60
+					}
+				}
+			},
+			yAxisLabel: {
+				type: Object,
+				default () {
+					return {
+						show: true,
+						color: "#333",
+						size: 24
+					}
+				}
+			},
+			//是否可滚动
+			scrollable: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				height: 0,
+				scrollViewH: 0,
+				sections: 0,
+				yAxisData: [],
+				activeIndex: -1,
+				activeIdx: -1,
+				tooltips: [],
+				tooltipShow: false,
+				timer: null,
+				dots: [],
+				lines: [],
+				/*========options============*/
+				/*
+					name: '', 
+					color: '',
+					source: []
+					colorFormatter:Function
+				*/
+				dataset: [],
+				xAxisValFormatter: null,
+				maxValue: 1
+			};
+		},
+		created() {
+			this.init()
+			this.activeIdx = this.currentIndex;
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.clearTimer()
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.clearTimer()
+		},
+		// #endif
+		methods: {
+			getYAxisVal(idx, index) {
+				let showVal = this.dataset[idx].source[index];
+				if (this.xAxisVal.formatter && typeof this.xAxisVal.formatter === 'function') {
+					showVal = this.xAxisVal.formatter(showVal)
+				} else if (this.xAxisValFormatter && typeof this.xAxisValFormatter === 'function') {
+					showVal = this.xAxisValFormatter(showVal)
+				}
+				return showVal
+			},
+			generateArray(start, end) {
+				return Array.from(new Array(end + 1).keys()).slice(start);
+			},
+			getValue(val) {
+				return val < 0 ? 0 : val;
+			},
+			getCoordinatePoint() {
+				const xAxis = [...this.xAxis];
+				const xSections = xAxis.length;
+				const ySections = this.yAxisData.length - 1;
+				const itemGap = this.scrollable ? (this.xAxisLine.itemGap || 120) : (this.width / xSections);
+				let dots = [];
+				let radius = (this.brokenDot.width || 12) / 2;
+
+				this.dataset.map((item, index) => {
+					let source = item.source || []
+					let dotArr = []
+					source.map((val, idx) => {
+						dotArr.push({
+							id: 'd' + idx,
+							x: this.getValue((0.5 + idx) * itemGap - radius),
+							y: this.getValue((val - this.min) / (this.maxValue - this.min) * (this
+									.yAxisLine
+									.itemGap || 60) *
+								ySections - radius)
+						})
+					})
+					dots.push({
+						id: 'dd' + index,
+						color: item.color,
+						source: dotArr
+					})
+				})
+				this.dots = dots;
+				this.drawLines(dots);
+			},
+			drawLines(dots) {
+				let lines = []
+				// dots是点的集合 : Array<{ x: number; y: number; }>
+				let radius = (this.brokenDot.width || 12) / 2;
+				dots.map((item, idx) => {
+					let dotArr = item.source;
+					let lineArr = [];
+					dotArr.map((dot, index) => {
+						// 最后一个点没有连线
+						if (!dotArr[index + 1]) return;
+						const AB = {
+							x: dotArr[index + 1].x - dot.x,
+							y: dotArr[index + 1].y - dot.y,
+							y1: dot.y - dotArr[index + 1].y
+						}
+						// 向量的模
+						const v = Math.sqrt(Math.pow(AB.x, 2) + Math.pow(AB.y, 2));
+						// 求出偏转角度
+						const angle = Math.atan2(AB.y1, AB.x) * (180 / Math.PI);
+						lineArr.push({
+							id: 'l' + index,
+							x: dot.x + radius,
+							y: dot.y + radius - 1,
+							width: v,
+							angle: AB.y1 > 0 ? Math.sqrt(Math.pow(angle, 2)) : -Math.sqrt(Math.pow(
+								angle,
+								2))
+						})
+					})
+					lines.push({
+						id: 'll' + idx,
+						color: item.color,
+						source: lineArr
+					})
+				})
+				this.lines = lines
+			},
+			init() {
+				this.maxValue = this.max;
+				let itemGap = this.yAxisLine.itemGap || 60;
+				let sections = this.yAxis.length - 1;
+				let yAxis = this.yAxis;
+				if (sections <= 0) {
+					sections = Math.ceil((this.max - this.min) / this.splitNumber)
+					let sectionsArr = this.generateArray(0, sections)
+
+					yAxis = sectionsArr.map(item => {
+						return {
+							value: item * this.splitNumber + this.min
+						}
+					})
+					this.maxValue = yAxis[yAxis.length - 1].value
+				}
+				this.yAxisData = yAxis;
+				this.sections = sections + 1;
+				this.height = itemGap * sections;
+				const valH = this.xAxisVal.height || 48;
+				this.scrollViewH = this.height + (this.xAxisLabel.height || 60) + valH;
+				this.getCoordinatePoint();
+			},
+			/*
+			dataset:折线图表数据
+			xAxisValFormatter :格式化折线拐点value值(此处传值是为了做兼容处理)
+			*/
+			draw(dataset, xAxisValFormatter) {
+				this.xAxisValFormatter = xAxisValFormatter || null;
+				this.dataset = dataset || [];
+				this.init();
+			},
+			clearTimer() {
+				clearTimeout(this.timer)
+				this.timer = null;
+			},
+			tooltipHandle(index) {
+				let data = [...this.dataset]
+				let tooltips = []
+				data.forEach(item => {
+					let color = item.color;
+					if (item.colorFormatter && typeof item.colorFormatter === 'function') {
+						color = item.colorFormatter(item.source[index])
+					}
+					tooltips.push({
+						color: color,
+						name: item.name,
+						val: item.source[index]
+					})
+				})
+				this.tooltips = tooltips;
+				this.clearTimer()
+				this.tooltipShow = true;
+				this.timer = setTimeout(() => {
+					this.tooltipShow = false
+				}, 5000)
+			},
+			dotClick(index, idx) {
+				this.activeIndex = index;
+				this.activeIdx = idx;
+				this.tooltipHandle(idx);
+				this.$emit('click', {
+					datasetIndex: index,
+					sourceIndex: idx,
+					...this.dataset[index]
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-charts__line-wrap {
+		position: relative;
+		transform: rotate(0deg) scale(1);
+		/* margin: 0 auto; */
+	}
+
+	.tui-line__legend {
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+	}
+
+	.tui-line__legend-item {
+		display: flex;
+		align-items: center;
+		margin-left: 24rpx;
+		margin-bottom: 30rpx;
+	}
+
+	.tui-line__legend-circle {
+		height: 20rpx;
+		width: 20rpx;
+		border-radius: 50%;
+		margin-right: 8rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-charts__line-box {
+		position: relative;
+		padding-left: 1px;
+		box-sizing: border-box;
+		transform-origin: 0 0;
+		overflow: visible;
+		transform:  scale(1);
+	}
+
+	.tui-line__scroll-view {
+		position: relative;
+		z-index: 10;
+		box-sizing: border-box;
+	}
+
+	.tui-charts__line {
+		min-width: 100%;
+		position: relative;
+		display: flex;
+		align-items: flex-end;
+		/* overflow: hidden; */
+		transform: rotate(0deg) scale(1);
+	}
+
+	.tui-line__between {
+		justify-content: space-between;
+	}
+
+	.tui-line__item {
+		height: 100%;
+		display: flex;
+		align-items: flex-end;
+		justify-content: center;
+		position: relative;
+		text-align: center;
+		box-sizing: border-box;
+		z-index: 10;
+		transition: all 0.3s;
+		flex-shrink: 0;
+	}
+
+	.tui-line__flex-1 {
+		flex: 1;
+	}
+
+	.tui-xAxis__tickmarks {
+		position: absolute;
+		right: 0;
+		width: 1px;
+		transform: translateY(100%);
+		bottom: 0;
+	}
+
+	.tui-yAxis__split-line {
+		position: absolute;
+		height: 100%;
+		width: 0;
+		border-right-width: 1px;
+		left: 50%;
+		transform: translateX(-50%);
+		z-index: 20;
+	}
+
+	.tui-line__xAxis-text {
+		width: 100%;
+		position: absolute;
+		left: 50%;
+		bottom: 0;
+		flex: 1;
+		transform: translate(-50%, 100%);
+		padding-top: 8rpx;
+		word-break: break-all;
+	}
+
+	.tui-line__border-left {
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 1px;
+		z-index: 11;
+	}
+
+	.tui-xAxis__line {
+		width: 100%;
+		height: 0;
+		border-top-width: 1px;
+		position: absolute;
+		left: 0;
+		display: flex;
+		align-items: center;
+
+	}
+
+	.tui-line__first {
+		z-index: 12;
+	}
+
+	.tui-yAxis__val {
+		transform: translateX(-100%);
+		padding-right: 12rpx;
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+	}
+
+	.tui-charts__line-dot {
+		position: absolute;
+		border-radius: 50%;
+		transition: all 0.3s;
+		z-index: 12;
+		border-width: 1px;
+		border-style: solid;
+		box-sizing: border-box;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-line__val {
+		width: 100%;
+		position: absolute;
+		top: 0;
+		left: 50%;
+		padding-bottom: 12rpx;
+		transform: translate(-50%, -100%);
+		-webkit-backface-visibility: hidden;
+		backface-visibility: hidden;
+		-webkit-font-smoothing: antialiased;
+		white-space: nowrap;
+		z-index: 20;
+	}
+
+	.tui-charts__dot-enlarge {
+		transform: scale(1.4);
+	}
+
+	.tui-charts__broken-line {
+		position: absolute;
+		transform-origin: 0 0;
+		transition: all 0.3s;
+		z-index: 10;
+		border-color: transparent;
+		box-sizing: border-box;
+		/* transform: translateZ(0); */
+		/* -webkit-backface-visibility:hidden; */
+	}
+
+	.tui-line__tooltip {
+		padding: 30rpx;
+		border-radius: 12rpx;
+		background-color: rgba(0, 0, 0, .6);
+		display: inline-block;
+		position: absolute;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		z-index: 20;
+		visibility: hidden;
+		opacity: 0;
+		transition: all 0.3s;
+	}
+
+	.tui-line__tooltip-show {
+		visibility: visible;
+		opacity: 1;
+	}
+
+	.tui-tooltip__title {
+		font-size: 30rpx;
+		color: #fff;
+		line-height: 30rpx;
+	}
+
+	.tui-line__tooltip-item {
+		display: flex;
+		align-items: center;
+		padding-top: 24rpx;
+		white-space: nowrap;
+	}
+
+	.tui-tooltip__val {
+		font-size: 24rpx;
+		line-height: 24rpx;
+		color: #fff;
+		margin-left: 6rpx;
+	}
+
+	.tui-tooltip__val-ml {
+		margin-left: 20rpx;
+	}
+</style>

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

@@ -0,0 +1,306 @@
+<template>
+	<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
+		<!-- #ifndef MP-ALIPAY -->
+		<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>
+		<!-- #endif -->
+
+		<!-- #ifdef MP-ALIPAY -->
+		<canvas class="tui-circular-default" :canvas-id="defaultCanvasId" :id="defaultCanvasId"
+			:style="{ width: diam*4 + 'px', height: (height || diam)*4 + 'px' }" v-if="defaultShow"></canvas>
+		<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId"
+			:style="{ width: diam*4 + 'px', height: (height || diam)*4 + 'px' }"></canvas>
+		<!-- #endif -->
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiCircularProgress',
+		emits: ['change', 'end'],
+		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: '#CCCCCC'
+			},
+			//进度条颜色
+			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() {
+			// #ifndef MP-WEIXIN || MP-QQ
+			let cid = `id01_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			let did = `id02_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			// #endif
+			return {
+				// #ifdef MP-WEIXIN || MP-QQ
+				progressCanvasId: 'progressCanvasId',
+				defaultCanvasId: 'defaultCanvasId',
+				// #endif
+				// #ifndef MP-WEIXIN || MP-QQ
+				progressCanvasId: cid,
+				defaultCanvasId: did,
+				// #endif
+				progressContext: null,
+				linearGradient: null,
+				//起始百分比
+				startPercentage: 0
+				// dpi
+				//pixelRatio: uni.getSystemInfoSync().pixelRatio
+			};
+		},
+		mounted() {
+			this.$nextTick(() => {
+				setTimeout(() => {
+					this.initDraw(true);
+				}, 50)
+			})
+		},
+		methods: {
+			//初始化绘制
+			initDraw(init) {
+				let start = this.activeMode === 'backwards' ? 0 : this.startPercentage;
+				start = start > this.percentage ? 0 : start;
+				if (this.defaultShow && init) {
+					this.drawDefaultCircular();
+				}
+				this.drawProgressCircular(start);
+			},
+			//默认(背景)圆环
+			drawDefaultCircular() {
+				let ctx = uni.createCanvasContext(this.defaultCanvasId, this);
+				let lineWidth = Number(this.lineWidth)
+				// #ifdef MP-ALIPAY
+				lineWidth = lineWidth * 4
+				// #endif
+				ctx.setLineWidth(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对象
+					let diam = Number(this.diam)
+					// #ifdef MP-ALIPAY
+					diam = diam * 4
+					// #endif
+					gradient = ctx.createLinearGradient(0, 0, diam, 0);
+					gradient.addColorStop('0', this.progressColor);
+					if (this.gradualColor) {
+						gradient.addColorStop('1', this.gradualColor);
+					}
+					// #ifdef APP-PLUS || MP
+					const res = uni.getSystemInfoSync();
+					if (!this.gradualColor && res.platform.toLocaleLowerCase() == 'android') {
+						gradient.addColorStop('1', this.progressColor);
+					}
+					// #endif
+					this.progressContext = ctx;
+					this.linearGradient = gradient;
+				}
+				let lineWidth = Number(this.lineWidth)
+				// #ifdef MP-ALIPAY
+				lineWidth = lineWidth * 4
+				// #endif
+				ctx.setLineWidth(lineWidth);
+				ctx.setStrokeStyle(gradient);
+				let time = this.percentage == 0 || this.duration < 50 ? 0 : this.duration / this.percentage;
+				if (this.percentage > 0) {
+					startPercentage = this.duration < 50 ? this.percentage - 1 : startPercentage;
+					startPercentage++;
+				}
+				if (this.fontShow) {
+					let fontSize = Number(this.fontSize)
+					// #ifdef MP-ALIPAY
+					fontSize = fontSize * 4
+					// #endif
+					ctx.setFontSize(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;
+					// #ifdef MP-ALIPAY
+					radius = radius * 4
+					// #endif
+					ctx.fillText(percentage, radius, radius);
+				}
+				if (this.percentage == 0 || (this.counterclockwise && startPercentage == 100)) {
+					ctx.draw();
+				} else {
+					let eAngle = ((2 * Math.PI) / 100) * startPercentage + this.sAngle;
+					this.drawArc(ctx, eAngle);
+				}
+				setTimeout(() => {
+					this.startPercentage = startPercentage;
+					if (startPercentage == this.percentage) {
+						this.$emit('end', {
+							canvasId: this.progressCanvasId,
+							percentage: startPercentage
+						});
+					} else {
+						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
+				let lineWidth = Number(this.lineWidth)
+				// #ifdef MP-ALIPAY
+				radius = radius * 4
+				lineWidth = lineWidth * 4
+				// #endif
+				ctx.arc(radius, radius, radius - lineWidth, this.sAngle, eAngle, this.counterclockwise);
+				ctx.stroke();
+				ctx.draw();
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-circular-container,
+	.tui-circular-default {
+		position: relative;
+
+	}
+
+	/* #ifdef MP-ALIPAY */
+	.tui-circular-default,
+	.tui-circular-progress {
+		zoom: 0.25;
+	}
+
+	/* #endif */
+
+
+	.tui-circular-progress {
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 10;
+	}
+</style>

+ 379 - 0
components/thorui/tui-code-input/tui-code-input.vue

@@ -0,0 +1,379 @@
+<template>
+	<view class="tui-code__input" :style="{marginTop:marginTop+'rpx',marginBottom:marginBottom+'rpx'}" @tap="onClick">
+		<view class="tui-code__input" :style="{paddingLeft:gap+'rpx',paddingRight:gap+'rpx'}">
+			<view class="tui-cinput__item"
+				:style="{width:width+'rpx',height:height+'rpx',background:background,borderRadius:radius+'rpx',borderColor:activeIndex===index || inputVal[index]?activeColor:borderColor,borderTopWidth:(borderType==1?borderWidth:0)+'rpx',borderLeftWidth:(borderType==1?borderWidth:0)+'rpx',borderRightWidth:(borderType==1?borderWidth:0)+'rpx',borderBottomWidth:(borderType==1 || borderType==2?borderWidth:0)+'rpx'}"
+				@tap="onTap" v-for="(item,index) in inputArr" :key="index">
+				<text class="tui-cinput__text"
+					:style="{width:width+'rpx',height:height+'rpx',fontSize:size+'rpx',lineHeight:height+'rpx',color:color,fontWeight:fontWeight}">{{password?(inputVal[index] ? '●':''):(inputVal[index] || '')}}</text>
+				<text class="tui-cinput__placeholder"
+					:style="{fontSize:size+'rpx',fontWeight:fontWeight}">{{password?(inputVal[index] ? '●':''):(inputVal[index] || '')}}</text>
+				<view class="tui-cinput__cursor" :class="{'tui-cinput__cursor-ani':activeIndex===index && focus}"
+					v-if="cursor && !disabled" :style="{height:cursorHeight+'rpx',background:cursorColor}">
+				</view>
+			</view>
+		</view>
+		<input :value="val" :password="password" :type="type" class="tui-cinput__hidden"
+			:class="{'tui-cinput__ali':ali}" @input="onInput" @blur="onBlur" :focus="focus" :maxlength="length"
+			:disabled="disabled" @confirm="onConfirm" @focus="onTap" />
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiCodeInput",
+		emits: ['complete', 'focus', 'input', 'blur', 'confirm'],
+		props: {
+			//组件外层左右间距
+			gap: {
+				type: [Number, String],
+				default: 80
+			},
+			marginTop: {
+				type: [Number, String],
+				default: 0
+			},
+			marginBottom: {
+				type: [Number, String],
+				default: 0
+			},
+			//初始值,不可超过length长度
+			value: {
+				type: String,
+				default: ''
+			},
+			//H5不支持动态切换type类型
+			type: {
+				type: String,
+				default: 'text'
+			},
+			password: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//获取焦点
+			isFocus: {
+				type: Boolean,
+				default: false
+			},
+			cursor: {
+				type: Boolean,
+				default: true
+			},
+			cursorColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			cursorHeight: {
+				type: [Number, String],
+				default: 60
+			},
+			//输入框个数
+			length: {
+				type: Number,
+				default: 4
+			},
+			width: {
+				type: [Number, String],
+				default: 108
+			},
+			height: {
+				type: [Number, String],
+				default: 108
+			},
+			background: {
+				type: String,
+				default: 'transparent'
+			},
+			//1-显示所有边框 2-只显示底部边框,3-无边框
+			borderType: {
+				type: [Number, String],
+				default: 1
+			},
+			borderColor: {
+				type: String,
+				default: '#eaeef1'
+			},
+			activeColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			borderWidth: {
+				type: [Number, String],
+				default: 2
+			},
+			radius: {
+				type: [Number, String],
+				default: 0
+			},
+			size: {
+				type: [Number, String],
+				default: 48
+			},
+			color: {
+				type: String,
+				default: '#333'
+			},
+			fontWeight: {
+				type: [Number, String],
+				default: 600
+			}
+		},
+		data() {
+			return {
+				inputArr: [],
+				inputVal: [],
+				focus: false,
+				activeIndex: -1,
+				ali: false,
+				val: ''
+			};
+		},
+		watch: {
+			length(val) {
+				const nums = Number(val);
+				if (nums !== this.inputArr.length) {
+					this.inputArr = this.getArr(nums)
+				}
+			},
+			value(val) {
+				this.focus = true;
+				val = val.replace(/\s+/g, "")
+				this.getVals(val)
+			},
+			isFocus(val) {
+				this.initFocus(val)
+			}
+		},
+		created() {
+			this.inputArr = this.getArr(Number(this.length))
+			let val = this.value.replace(/\s+/g, "")
+			this.getVals(val, true)
+		},
+		mounted() {
+			setTimeout(() => {
+				this.initFocus(this.isFocus)
+			}, 300)
+		},
+		methods: {
+			initFocus(val) {
+				if (this.disabled) return;
+				if (val && this.activeIndex === -1) {
+					this.activeIndex = 0
+				}
+				if (!this.value && !val) {
+					this.activeIndex = -1
+				}
+				this.$nextTick(() => {
+					this.focus = val
+				})
+			},
+			getArr(end) {
+				return Array.from(new Array(end + 1).keys()).slice(1);
+			},
+			getVals(val, init = false) {
+				this.val = val
+				if (!val) {
+					this.inputVal = []
+					this.activeIndex = init ? -1 : 0;
+				} else {
+					let vals = val.split('')
+					let arr = []
+					this.inputArr.forEach((item, index) => {
+						arr.push(vals[index] || '')
+					})
+					this.inputVal = arr
+					const len = vals.length;
+					this.activeIndex = len > this.length ? this.length : len;
+					if (len === this.length) {
+						this.$emit('complete', {
+							detail: {
+								value: val
+							}
+						})
+						this.focus = false;
+						uni.hideKeyboard()
+					}
+				}
+			},
+			onTap() {
+				if (this.disabled) return;
+				this.focus = true;
+				if (this.activeIndex === -1) {
+					this.activeIndex = 0
+				}
+				if (this.activeIndex === this.length) {
+					this.activeIndex--;
+				}
+				this.$emit('focus', {})
+			},
+			onInput(e) {
+				let value = e.detail.value;
+				value = value.replace(/\s+/g, "")
+				this.getVals(value)
+				this.$emit('input', {
+					detail: {
+						value: value
+					}
+				})
+			},
+			onBlur(e) {
+				let value = e.detail.value;
+				value = value.replace(/\s+/g, "")
+				this.focus = false
+				// #ifdef MP-ALIPAY
+				this.ali = false
+				// #endif
+				if (!value) {
+					this.activeIndex = -1;
+				}
+				this.$emit('blur', {
+					detail: {
+						value: value
+					}
+				})
+			},
+			onConfirm(e) {
+				this.focus = false;
+				uni.hideKeyboard()
+				this.$emit('confirm', e)
+			},
+			onClick() {
+				// #ifdef MP-ALIPAY
+				setTimeout(() => {
+					this.ali = true
+				}, 50)
+				// #endif
+			},
+			clear() {
+				this.val = ''
+				this.inputVal = []
+				this.activeIndex = -1;
+				this.$nextTick(() => {
+					this.onTap()
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-code__input {
+		position: relative;
+		/* #ifdef MP-BAIDU */
+		max-width: 100%;
+		overflow: hidden;
+		/* #endif */
+	}
+
+	.tui-code__input {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+		justify-content: space-between;
+	}
+
+
+	.tui-cinput__item {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		border-style: solid;
+		position: relative;
+		overflow: hidden;
+	}
+
+	.tui-cinput__text {
+		position: absolute;
+		left: 0;
+		top: 0;
+		flex: 1;
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		text-align: center;
+	}
+
+	.tui-cinput__placeholder {
+		text-align: center;
+		opacity: 0;
+	}
+
+	.tui-cinput__cursor {
+		border-radius: 2px;
+		width: 0;
+	}
+
+	.tui-cinput__cursor-ani {
+		width: 2px;
+		animation: ani_cursor 1s infinite steps(1, start);
+	}
+
+	@keyframes ani_cursor {
+		0% {
+			opacity: 0;
+		}
+
+		50% {
+			opacity: 1;
+		}
+
+		100% {
+			opacity: 0;
+		}
+	}
+
+	.tui-cinput__hidden {
+		position: absolute;
+		left: 0;
+		top: 0;
+		/* #ifndef MP */
+		right: 0;
+		bottom: 0;
+		/* #endif */
+		/* #ifndef MP-WEIXIN || MP-QQ */
+		width: 100%;
+		height: 100%;
+		/* #endif */
+		z-index: 2;
+		/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+		height: 0;
+		width: 0;
+		border: none;
+		/* #endif */
+		margin: 0;
+		padding: 0;
+		opacity: 0;
+		/* #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO */
+		font-size: 0;
+		/* #endif */
+
+		/* #ifdef MP-BAIDU */
+		transform: scaleX(2);
+		transform-origin: 100% center;
+		/* #endif */
+		color: transparent;
+	}
+
+	/* #ifdef MP-ALIPAY */
+	.tui-cinput__ali {
+		height: 0;
+		width: 0;
+	}
+
+	/* #endif */
+</style>

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

@@ -0,0 +1,167 @@
+<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",
+		emits: ['click'],
+		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>

+ 234 - 0
components/thorui/tui-countdown-verify/tui-countdown-verify.vue

@@ -0,0 +1,234 @@
+<template>
+	<view
+		class="tui-countdown__verify"
+		:class="{ 'tui-verify__opacity': status > 1 && isOpacity }"
+		:style="{ width: width, height: height, padding: padding, margin: margin, borderRadius: radius, fontSize: size + 'rpx', color: color, background: background }"
+		:hover-class="hover && status == 1 ? 'tui-verify__opacity' : ''"
+		:hover-stay-time="150"
+		@tap.stop="sendCode"
+	>
+		{{ showText }}
+		<view class="tui-verify__line" :style="{ borderWidth: borderWidth, borderColor: borderColor, borderRadius: radius }"></view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiCountdownVerify',
+	emits: ['send','countdown','end'],
+	props: {
+		//发送前显示文本
+		text: {
+			type: String,
+			default: '发送验证码'
+		},
+		//发送中显示文本
+		sendText: {
+			type: String,
+			default: '请稍候...'
+		},
+		//发送后显示文本(前面会自动加上seconds)
+		countdownText: {
+			type: String,
+			default: 's后获取'
+		},
+		//倒计时秒数
+		seconds: {
+			type: Number,
+			default: 60
+		},
+		//宽度
+		width: {
+			type: String,
+			default: '182rpx'
+		},
+		//高度
+		height: {
+			type: String,
+			default: '56rpx'
+		},
+		padding: {
+			type: String,
+			default: '0'
+		},
+		margin: {
+			type: String,
+			default: '0'
+		},
+		//圆角
+		radius: {
+			type: String,
+			default: '6rpx'
+		},
+		//字体大小 rpx
+		size: {
+			type: Number,
+			default: 24
+		},
+		//字体颜色
+		color: {
+			type: String,
+			default: '#5677fc'
+		},
+		//背景色
+		background: {
+			type: String,
+			default: 'transparent'
+		},
+		//边框宽度
+		borderWidth: {
+			type: String,
+			default: '1px'
+		},
+		//边框颜色
+		borderColor: {
+			type: String,
+			default: '#5677fc'
+		},
+		//倒计时的时候是否改变opacity值
+		isOpacity: {
+			type: Boolean,
+			default: true
+		},
+		//是否需要点击效果
+		hover: {
+			type: Boolean,
+			default: true
+		},
+		//短信发送成功(改变数值且数值大于0表示发送成功,多次发送数值递增即可)
+		successVal: {
+			type: Number,
+			default: 0
+		},
+		//重置组件状态(改变数值且数值大于0,多次重置数值递增即可)
+		resetVal: {
+			type: Number,
+			default: 0
+		},
+		//是否默认为倒计时状态
+		start:{
+			type:Boolean,
+			default:false
+		},
+		//自定义参数
+		params: {
+			type: [Number, String],
+			default: 0
+		}
+	},
+	data() {
+		return {
+			showText: '',
+			//1-发送前,2-发送中 3-发送成功,倒计时
+			status: 1,
+			countdownTimer: null
+		};
+	},
+	created() {
+		if(this.start){
+			this.doLoop();
+		}else{
+			this.showText = this.text;
+			this.clearTimer();
+		}
+	},
+	// #ifndef VUE3
+	beforeDestroy() {
+		this.clearTimer();
+	},
+	// #endif
+	// #ifdef VUE3
+	beforeUnmount() {
+		this.clearTimer();
+	},
+	// #endif
+	watch: {
+		successVal(val) {
+			if (val && val > 0) {
+				this.doLoop();
+			}
+		},
+		resetVal(val) {
+			if (val && val > 0) {
+				this.reset();
+			}
+		}
+	},
+	methods: {
+		sendCode() {
+			if (this.status > 1) return;
+			this.clearTimer();
+			this.status = 2;
+			this.showText = this.sendText;
+			this.$emit('send', {
+				params: this.params
+			});
+		},
+		doLoop: function() {
+			this.clearTimer();
+			this.status = 3;
+			let seconds = this.seconds || 60;
+			this.showText = seconds + this.countdownText;
+			this.countdownTimer = setInterval(() => {
+				if (seconds > 1) {
+					--seconds;
+					this.showText = seconds + this.countdownText;
+					//倒计时
+					this.$emit('countdown', {
+						seconds: seconds,
+						params: this.params
+					});
+				} else {
+					this.reset();
+					//倒计时结束
+					this.$emit('end', {
+						params: this.params
+					});
+				}
+			}, 1000);
+		},
+		//验证码发送成功
+		success() {
+			this.doLoop();
+		},
+		//重置发送组件
+		reset() {
+			this.clearTimer();
+			this.showText = this.text;
+			this.status = 1;
+		},
+		clearTimer() {
+			clearInterval(this.countdownTimer);
+			this.countdownTimer = null;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-countdown__verify {
+	position: relative;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	white-space: nowrap;
+	box-sizing: border-box;
+}
+
+.tui-verify__opacity {
+	opacity: 0.5;
+}
+
+.tui-verify__line {
+	position: absolute;
+	width: 200%;
+	height: 200%;
+	transform-origin: 0 0;
+	transform: scale(0.5, 0.5) translateZ(0);
+	box-sizing: border-box;
+	border-style: solid;
+	left: 0;
+	top: 0;
+	pointer-events: none;
+}
+</style>

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

@@ -0,0 +1,336 @@
+<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',
+		emits: ['end', 'time'],
+		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, String],
+				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();
+			let seconds = Number(this.time || 0);
+			if (seconds > 0) {
+				this.doLoop();
+			}
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.clearTimer();
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.clearTimer();
+		},
+		// #endif
+		methods: {
+			getWidth: function(num, width) {
+				return num > 99 ? (width / 2) * num.toString().length : width;
+			},
+			clearTimer() {
+				clearInterval(this.countdown);
+				this.countdown = null;
+			},
+			endOfTime(isStop = false) {
+				this.ani = false;
+				this.clearTimer();
+				if (!isStop) {
+					this.$emit('end', {});
+				}
+			},
+			doLoop: function() {
+				let seconds = Number(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;
+				}
+				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>

+ 489 - 0
components/thorui/tui-cropper/tui-cropper.vue

@@ -0,0 +1,489 @@
+<template>
+	<view class="tui-cropper__box" @touchmove.stop.prevent="stop">
+		<image @load="imageLoad" @error="imageLoad" @touchstart="parse.touchstart" @touchmove="parse.touchmove"
+			@touchend="parse.touchend" :data-minScale="minScale" :data-maxScale="maxScale" :style="{
+				width: (imgWidth ? imgWidth : width) + 'px',
+				height: imgHeight ? imgHeight + 'px' : 'auto',
+				transitionDuration: (animation ? 0.3 : 0) + 's'
+			}" class="tui-cropper__image" :class="{'tui-cropper__image-hidden':!imageUrl}" :src="imageUrl" mode="widthFix">
+		</image>
+		<view class="tui-backdrop__cropper"
+			:style="{ width: width + 'px', height: height + 'px', borderRadius: isRound ? '50%' : '0' }">
+			<view class="tui-cropper__border" :change:prop="parse.propsChange" :prop="props" :data-width="width"
+				:data-height="height" :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"
+				:style="{ borderRadius: isRound ? '50%' : '0', border: border }"></view>
+		</view>
+		<canvas canvas-id="tui-image__cropper" id="tui-image__cropper" :disable-scroll="true"
+			:style="{ width: width * scaleRatio + 'px', height: 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-cropper.wxs" module="parse" lang="wxs"></script>
+<script>
+	export default {
+		name: 'tuiCropper',
+		emits: ['ready', 'cropper', 'imageLoad', 'initAngle'],
+		props: {
+			//图片路径
+			imageUrl: {
+				type: String,
+				default: ''
+			},
+			//裁剪框高度 px
+			height: {
+				type: Number,
+				default: 280
+			},
+			//裁剪框宽度 px
+			width: {
+				type: Number,
+				default: 280
+			},
+			//是否为圆形裁剪框
+			isRound: {
+				type: Boolean,
+				default: true
+			},
+			//裁剪框边框
+			border: {
+				type: String,
+				default: '1px solid #fff'
+			},
+			//生成的图片尺寸相对剪裁框的比例
+			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
+			},
+			//自定义操作栏(为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,
+				cutX: 0, //画布x轴起点
+				cutY: 0, //画布y轴起点0
+				imgWidth: 0,
+				imgHeight: 0,
+				scale: 1, //图片缩放比
+				angle: 0, //图片旋转角度
+				animation: false, //是否开启图片过渡效果
+				animationTime: 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);
+						this.angleChange++;
+						this.props = `3,${this.angleChange}`;
+					},
+					fail: err => {
+						this.imgComputeSize();
+						this.angleChange++;
+						this.props = `3,${this.angleChange}`;
+					}
+				});
+			},
+			rotateAngle(val) {
+				this.animation = true;
+				this.angle = val;
+				this.angleChanged(val);
+			},
+			animation(val) {
+				//开启过渡220毫秒之后自动关闭
+				clearTimeout(this.animationTime);
+				if (val) {
+					this.animationTime = setTimeout(() => {
+						this.animation = false;
+					}, 220);
+				}
+			},
+			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.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.width * this.scaleRatio,
+							height: Math.round(this.height * this.scaleRatio),
+							destWidth: this.width * this.scaleRatio,
+							destHeight: Math.round(this.height) * this.scaleRatio,
+							fileType: 'png',
+							quality: this.quality
+						};
+						let data = {
+							url: '',
+							base64: '',
+							width: this.width * this.scaleRatio,
+							height: this.height * 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.apFilePath;
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								}
+							});
+						}
+						// #endif
+
+						// #ifndef MP-ALIPAY
+						let isBase64=this.isBase64
+						// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+						isBase64 = false;
+						// #endif
+						if (isBase64) {
+							uni.canvasGetImageData({
+									canvasId: 'tui-image__cropper',
+									x: 0,
+									y: 0,
+									width: this.width * this.scaleRatio,
+									height: Math.round(this.height * 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
+					});
+				};
+				draw();
+			},
+			change(e) {
+				this.cutX = e.cutX || 0;
+				this.cutY = e.cutY || 0;
+				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', {});
+			},
+			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.windowHeight ? this.sysInfo : uni.getSystemInfoSync();
+					imgWidth = sys.windowWidth;
+					imgHeight = 0;
+				}
+				this.imgWidth = imgWidth;
+				this.imgHeight = imgHeight;
+				this.sizeChange++;
+				this.props = `2,${this.sizeChange}`;
+			},
+			imageLoad(e) {
+				this.imageReset();
+				uni.hideLoading();
+				this.$emit('imageLoad', {});
+			},
+			moveStop() {
+				clearTimeout(this.TIME_CUT_CENTER);
+				this.TIME_CUT_CENTER = setTimeout(() => {
+					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 (val % 90) {
+					this.angle = Math.round(val / 90) * 90;
+				}
+				this.angleChange++;
+				this.props = `3,${this.angleChange}`;
+			},
+			setAngle() {
+				this.animation = true;
+				this.angle = this.angle + 90;
+				this.angleChanged(this.angle);
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-cropper__box {
+		width: 100vw;
+		height: 100vh;
+		background-color: rgba(0, 0, 0, 0.7);
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 1;
+	}
+
+	.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__image-hidden {
+		visibility: hidden;
+		opacity: 0;
+	}
+
+	.tui-cropper__canvas {
+		position: fixed;
+		z-index: 10;
+		left: -2000px;
+		top: -2000px;
+		pointer-events: none;
+	}
+
+	.tui-backdrop__cropper {
+		position: fixed;
+		z-index: 4;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+		border: 3000px solid rgba(0, 0, 0, 0.55);
+		pointer-events: none;
+	}
+
+	.tui-cropper__border {
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 100%;
+		box-sizing: border-box;
+		pointer-events: none;
+	}
+
+	.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>

+ 321 - 0
components/thorui/tui-cropper/tui-cropper.wxs

@@ -0,0 +1,321 @@
+var cropper = {
+	cutX: 0, //画布x轴起点
+	cutY: 0, //画布y轴起点0
+	touchRelative: [{
+		x: 0,
+		y: 0
+	}], //手指或鼠标和图片中心的相对位置
+	hypotenuseLength: 0, //双指触摸时斜边长度
+	flagEndTouch: false, //是否结束触摸
+	canvasWidth: 0,
+	canvasHeight: 0,
+	imgWidth: 0, //图片宽度
+	imgHeight: 0, //图片高度
+	scale: 1, //图片缩放比
+	angle: 0, //图片旋转角度
+	imgTop: 0, //图片上边距
+	imgLeft: 0, //图片左边距
+	windowHeight: 0,
+	windowWidth: 0,
+	init: true
+}
+
+function bool(str) {
+	return str === 'true' || str == true ? true : false
+}
+
+function touchstart(e, ins) {
+	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; //截取的框左边距
+	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 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
+			}
+		];
+		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 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;
+	}
+}
+
+/**
+ * 图片边缘检测-缩放
+ */
+function imgMarginDetectionScale(ins, delay) {
+	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) {
+	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;
+	if (!delay || delay === 'null') {
+		imgTransform(ins);
+	}
+}
+
+
+//改变截取框大小
+function computeCutSize(ins) {
+	if (cropper.canvasWidth > cropper.windowWidth) {
+		cropper.canvasWidth = cropper.windowWidth;
+	} else if (cropper.canvasWidth + cropper.cutX > cropper.windowWidth) {
+		cropper.cutX = cropper.windowWidth - cropper.cutX;
+	}
+	if (cropper.canvasHeight > cropper.windowHeight) {
+		cropper.canvasHeight = cropper.windowHeight;
+	} else if (cropper.canvasHeight + cropper.cutY > cropper.windowHeight) {
+		cropper.cutY = cropper.windowHeight - cropper.cutY;
+	}
+}
+
+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 (!ins) return;
+	computeCutSize(ins);
+}
+
+function canvasHeight(ins) {
+	if (!ins) return;
+	computeCutSize(ins);
+}
+
+function updateData(ins) {
+	if (!ins) return;
+	ins.callMethod('change', {
+		cutX: cropper.cutX,
+		cutY: cropper.cutY,
+		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.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.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,
+	propsChange: propsChange
+}

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

@@ -0,0 +1,655 @@
+<template>
+	<view class="tui-datetime-picker">
+		<view class="tui-mask" :class="{ 'tui-mask-show': isShow }" @touchmove.stop.prevent="stop" catchtouchmove="stop"
+			@tap="maskClick"></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-pickerdate__title" :style="{fontSize:titleSize+'rpx',color:titleColor}">{{title}}
+				</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 || type==8">年</view>
+				<view class="tui-date-unit" v-if="type < 4 || type == 7 || type==8">月</view>
+				<view class="tui-date-unit" v-if="type == 1 || type == 2 || type == 7 || type==8">日</view>
+				<view class="tui-date-unit" v-if="type == 1 || type == 4 || type == 5 || type == 7 || type==8">时</view>
+				<view class="tui-date-unit" v-if="(type == 1 || type > 3) && type!=8">分</view>
+				<view class="tui-date-unit" v-if="type > 4 && type !=8">秒</view>
+			</view>
+			<view @touchstart.stop="pickerstart" class="tui-date__picker-body"
+				:style="{ backgroundColor: bodyBackground,height:height+'rpx' }">
+				<picker-view :value="value" @change="change" class="tui-picker-view">
+					<picker-view-column v-if="!reset && (type < 4 || type == 7 || type==8)">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in years" :key="index">
+							{{ item }}
+							<text class="tui-date__unit-text" v-if="!unitTop">年</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type < 4 || type == 7 || type==8)">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in months" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-date__unit-text" v-if="!unitTop">月</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type == 2 || type == 7 || type==8)">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in days" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-date__unit-text" v-if="!unitTop">日</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type == 4 || type == 5 || type == 7 || type==8)">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in hours" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-date__unit-text" v-if="!unitTop">时</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && (type == 1 || type > 3)  && type!=8">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in minutes" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-date__unit-text" v-if="!unitTop">分</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!reset && type > 4 && type!=8">
+						<view class="tui-date__column-item" :class="{ 'tui-font-size_32': !unitTop && type == 7 }"
+							v-for="(item, index) in seconds" :key="index">
+							{{ formatNum(item) }}
+							<text class="tui-date__unit-text" v-if="!unitTop">秒</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiDatetime',
+		emits: ['cancel', 'confirm'],
+		props: {
+			//1-日期+时间(年月日+时分) 2-日期(年月日) 3-日期(年月) 4-时间(时分) 5-时分秒 6-分秒 7-年月日 时分秒 8-年月日+小时
+			type: {
+				type: Number,
+				default: 1
+			},
+			//年份区间
+			startYear: {
+				type: Number,
+				default: 1980
+			},
+			//年份区间
+			endYear: {
+				type: Number,
+				default: 2050
+			},
+			//显示标题
+			title: {
+				type: String,
+				default: ''
+			},
+			//标题字体大小
+			titleSize: {
+				type: [Number, String],
+				default: 34
+			},
+			//标题字体颜色
+			titleColor: {
+				type: String,
+				default: '#333'
+			},
+			//"取消"字体颜色
+			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'
+			},
+			height: {
+				type: [Number, String],
+				default: 520
+			},
+			//点击遮罩 是否可关闭
+			maskClosable: {
+				type: Boolean,
+				default: true
+			}
+
+		},
+		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,
+				isEnd: true
+			};
+		},
+		mounted() {
+			setTimeout(() => {
+				this.initData();
+			}, 20)
+		},
+		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();
+				}, 20);
+			}
+		},
+		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;
+			},
+			getCharCount(str) {
+				let regex = new RegExp('/', 'g');
+				let result = str.match(regex);
+				return !result ? 0 : result.length;
+			},
+			//日期时间处理
+			initSelectValue() {
+				let fdate = this.setDateTime.replace(/\-/g, '/');
+				if (this.type == 3 && this.getCharCount(fdate) === 1) {
+					fdate += '/01'
+				}
+				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;
+					case 8:
+						this.value = [0, 0, 0, 0];
+						this.setYears();
+						this.setMonths();
+						this.setDays();
+						this.setHours();
+						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();
+				totalDays = !totalDays || totalDays < 1 ? 1 : totalDays
+				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 = 0
+					if (this.type == 8) {
+						index = this.value.length - 1
+					} else {
+						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', {});
+			},
+			maskClick() {
+				if (!this.maskClosable) return;
+				this.hide()
+			},
+			change(e) {
+				if(!this.isShow) return;
+				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;
+					case 8:
+						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]];
+						break;
+					default:
+						break;
+				}
+				this.isEnd = true
+			},
+			selectResult() {
+				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;
+					case 8:
+						result = {
+							year: year,
+							month: month,
+							day: day,
+							hour: hour,
+							result: `${year}-${month}-${day} ${hour}:00`
+						};
+						break;
+					default:
+						break;
+				}
+				this.$emit('confirm', result);
+			},
+			waitFix() {
+				if (this.isEnd) {
+					this.selectResult()
+				} else {
+					setTimeout(() => {
+						this.waitFix()
+					}, 50)
+				}
+			},
+			btnFix() {
+				setTimeout(() => {
+					this.waitFix()
+					this.hide();
+				}, 80);
+			},
+			pickerstart() {
+				this.isEnd = false
+			}
+		}
+	};
+</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-date__picker-body {
+		width: 100%;
+		/* height: 520rpx; */
+		overflow: hidden;
+	}
+
+	.tui-date__column-item {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 36rpx;
+		color: #333;
+	}
+
+	.tui-font-size_32 {
+		font-size: 32rpx !important;
+	}
+
+	.tui-date__unit-text {
+		font-size: 24rpx !important;
+		padding-left: 8rpx;
+	}
+
+	.tui-btn-picker {
+		padding: 16rpx;
+		box-sizing: border-box;
+		text-align: center;
+		text-decoration: none;
+		flex-shrink: 0;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-opacity {
+		opacity: 0.5;
+	}
+
+	.tui-pickerdate__title {
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		flex: 1;
+		padding: 0 30rpx;
+		box-sizing: border-box;
+		text-align: center;
+	}
+</style>

+ 193 - 0
components/thorui/tui-dialog/tui-dialog.vue

@@ -0,0 +1,193 @@
+<template>
+	<view>
+		<view v-if="show" class="tui-dialog" :style="{background:backgroundColor,borderRadius:radius}" @tap.stop="stopEvent">
+			<view class="tui-dialog__hd">
+				<view class="tui-dialog__title" :style="{color:titleColor}">{{title}}
+					<slot name="title"></slot>
+				</view>
+			</view>
+			<view class="tui-dialog__bd" :style="{color:contentColor}">
+				<slot name="content"></slot>
+			</view>
+			<view class="tui-dialog__ft">
+				<block v-if="buttons && buttons.length">
+					<view v-for="(item,index) in buttons" :key="index" :style="{color:item.color || '#333'}" class="tui-dialog__btn" :data-index="index" @tap="buttonTap">{{item.text}}</view>
+				</block>
+				<slot name="footer" v-else></slot>
+			</view>
+		</view>
+		<view @tap="close" class="tui-dialog__mask" :class="{'tui-mask_hidden':!show}" v-if="mask"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:'tuiDialog',
+		emits: ['click','close'],
+		props: {
+			title: {
+				type: String,
+				default: ''
+			},
+			maskClosable: {
+				type: Boolean,
+				default: true
+			},
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			show: {
+				type: Boolean,
+				default: false
+			},
+			buttons: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			backgroundColor:{
+				type:String,
+				default:'#fff'
+			},
+			radius:{
+				type:String,
+				default:'12px'
+			},
+			titleColor:{
+				type:String,
+				default:'#333'
+			},
+			contentColor:{
+				type:String,
+				default:'#999'
+			}
+		},
+		methods: {
+			buttonTap(e) {
+				const {
+					index
+				} = e.currentTarget.dataset;
+				this.$emit('click', {
+					index,
+					item: this.buttons[index]
+				});
+			},
+
+			close() {
+				if (!this.maskClosable) return;
+				this.$emit('close', {});
+			},
+
+			stopEvent() {}
+		}
+	}
+</script>
+
+<style>
+	.tui-dialog {
+		position: fixed;
+		z-index: 5000;
+		top: 50%;
+		left: 16px;
+		right: 16px;
+		transform: translateY(-50%);
+		text-align: center;
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+		max-height: 90%;
+	}
+
+	.tui-dialog__hd {
+		padding: 32px 24px 16px
+	}
+
+	.tui-dialog__title {
+		font-weight: 700;
+		font-size: 17px;
+		line-height: 1.4
+	}
+
+	.tui-dialog__bd {
+		overflow-y: auto;
+		-webkit-overflow-scrolling: touch;
+		padding: 0 24px;
+		margin-bottom: 32px;
+		font-size: 15px;
+		line-height: 1.4;
+		word-wrap: break-word;
+		-webkit-hyphens: auto;
+		hyphens: auto;
+	}
+
+
+	.tui-dialog__ft {
+		display: flex;
+		position: relative;
+		line-height: 56px;
+		min-height: 56px;
+		font-size: 17px
+	}
+
+	.tui-dialog__ft:after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		top: 0;
+		right: 0;
+		height: 1px;
+		border-top: 1px solid rgba(0, 0, 0, .1);
+		transform-origin: 0 0;
+		transform: scaleY(.5)
+	}
+
+	.tui-dialog__btn {
+		display: block;
+		flex: 1;
+		font-weight: 700;
+		text-decoration: none;
+		-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+		position: relative
+	}
+
+	.tui-dialog__btn:active {
+		background-color: #ECECEC
+	}
+
+	.tui-dialog__btn:first-child::after {
+		width: 0;
+		border-left: 0;
+	}
+
+	.tui-dialog__btn::after {
+		content: " ";
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 1px;
+		bottom: 0;
+		border-left: 1px solid rgba(0,0,0,.1);
+		transform-origin: 0 0;
+		transform: scaleX(.5)
+	}
+
+	.tui-dialog__mask.tui-mask_hidden {
+		opacity: 0;
+		transform: scale3d(1, 1, 0)
+	}
+
+	.tui-dialog__mask {
+		position: fixed;
+		z-index: 1000;
+		top: 0;
+		right: 0;
+		left: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, .6);
+		opacity: 1;
+		transform: scale3d(1, 1, 1);
+		transition: all .2s ease-in
+	}
+</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: 1px;
+		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>

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

@@ -0,0 +1,140 @@
+<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',
+	emits: ['close'],
+	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: 990
+		},
+		//mask z-index
+		maskZIndex: {
+			type: [Number, String],
+			default: 980
+		},
+		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>

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

@@ -0,0 +1,276 @@
+<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':isHidden}">
+				<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",
+		emits: ['click'],
+		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,
+				isHidden: true,
+				timer: null
+			};
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			clearTimeout(this.timer)
+			this.timer = null
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			clearTimeout(this.timer)
+			this.timer = null
+		},
+		// #endif
+		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.isHidden = 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.isHidden = true
+					}, 200)
+				}
+			},
+			handleClickCancel: function() {
+				if (!this.maskClosable) return;
+				this.isOpen = false
+			}
+		}
+	}
+</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>

+ 277 - 0
components/thorui/tui-form-button/tui-form-button.vue

@@ -0,0 +1,277 @@
+<template>
+	<view class="tui-button__container" :style="{width: width,height: height,margin:margin,borderRadius: radius}"
+		@touchstart="handleStart" @touchend="handleClick" @touchcancel="handleEnd">
+		<button class="tui-button" :class="[
+				bold ? 'tui-text__bold' : '',
+				time && (plain || link) ? 'tui-button__opacity' : '',
+				disabled && !disabledBackground ? 'tui-button__opacity' : '',
+				!width || width==='100%' || width===true?'tui-button__flex-1':'',
+				time && !plain && !link ? 'tui-button__active' : ''
+			]" :style="{
+				width: width,
+				height: height,
+				lineHeight: height,
+				background: disabled && disabledBackground ? disabledBackground : (plain ? 'transparent' : background),
+				borderWidth:borderWidth,
+				borderColor: borderColor ? borderColor : disabled && disabledBackground ? disabledBackground : (link?'transparent':background),
+				borderRadius: radius,
+				fontSize: size + 'rpx',
+				color: disabled && disabledBackground ? disabledColor : color
+			}" :loading="loading" :form-type="formType" :open-type="openType" @getuserinfo="bindgetuserinfo"
+			@getphonenumber="bindgetphonenumber" @contact="bindcontact" @error="binderror"
+			@opensetting="bindopensetting" :disabled="disabled" :scope="scope" @tap.stop="handleTap">
+			<text class="tui-button__text" :class="{'tui-text__bold':bold}" v-if="text"
+				:style="{fontSize: size + 'rpx',lineHeight:size + 'rpx',color: disabled && disabledBackground ? disabledColor : color}">{{text}}</text>
+			<slot></slot>
+		</button>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tui-form-button',
+		emits: ['click', 'getuserinfo', 'contact', 'getphonenumber', 'error', 'opensetting'],
+		// #ifndef VUE3
+		// #ifdef MP-WEIXIN
+		behaviors: ['wx://form-field-button'],
+		// #endif
+		// #endif
+		props: {
+			//按钮背景色
+			background: {
+				type: String,
+				default: '#5677fc'
+			},
+			//按钮显示文本
+			text: {
+				type: String,
+				default: ''
+			},
+			//按钮字体颜色
+			color: {
+				type: String,
+				default: '#fff'
+			},
+			//按钮禁用背景色
+			disabledBackground: {
+				type: String,
+				default: ''
+			},
+			//按钮禁用字体颜色
+			disabledColor: {
+				type: String,
+				default: ''
+			},
+			borderWidth: {
+				type: String,
+				// #ifdef APP-NVUE
+				default: '0.5px'
+				// #endif
+				// #ifndef APP-NVUE
+				default: '1rpx'
+				// #endif
+			},
+			borderColor: {
+				type: String,
+				default: ''
+			},
+			//宽度
+			width: {
+				type: String,
+				default: '100%'
+			},
+			//高度
+			height: {
+				type: String,
+				default: '96rpx'
+			},
+			//字体大小,单位rpx
+			size: {
+				type: [Number, String],
+				default: 32
+			},
+			bold: {
+				type: Boolean,
+				default: false
+			},
+			margin: {
+				type: String,
+				default: '0'
+			},
+			//圆角
+			radius: {
+				type: String,
+				default: '6rpx'
+			},
+			plain: {
+				type: Boolean,
+				default: false
+			},
+			link: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			loading: {
+				type: Boolean,
+				default: false
+			},
+			formType: {
+				type: String,
+				default: ''
+			},
+			openType: {
+				type: String,
+				default: ''
+			},
+			//支付宝小程序 
+			//当 open-type 为 getAuthorize 时,可以设置 scope 为:phoneNumber、userInfo
+			scope: {
+				type: String,
+				default: ''
+			},
+			index: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		data() {
+			return {
+				time: 0,
+				trigger: false,
+				tap: false
+			};
+		},
+		methods: {
+			handleStart() {
+				if (this.disabled) return;
+				this.trigger = false;
+				this.tap = true;
+				if (new Date().getTime() - this.time <= 150) return;
+				this.trigger = true;
+				this.time = new Date().getTime();
+			},
+			handleClick() {
+				if (this.disabled || !this.trigger) return;
+				this.time = 0;
+			},
+			handleTap() {
+				if (this.disabled) return;
+				this.$emit('click', {
+					index: Number(this.index)
+				});
+			},
+			handleEnd() {
+				if (this.disabled) return;
+				setTimeout(() => {
+					this.time = 0;
+				}, 150);
+			},
+			bindgetuserinfo({
+				detail = {}
+			} = {}) {
+				this.$emit('getuserinfo', detail);
+			},
+			bindcontact({
+				detail = {}
+			} = {}) {
+				this.$emit('contact', detail);
+			},
+			bindgetphonenumber({
+				detail = {}
+			} = {}) {
+				this.$emit('getphonenumber', detail);
+			},
+			binderror({
+				detail = {}
+			} = {}) {
+				this.$emit('error', detail);
+			},
+			bindopensetting({
+				detail = {}
+			} = {}) {
+				this.$emit('opensetting', detail);
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-button__container {
+		position: relative;
+	}
+
+	.tui-button {
+		/* #ifdef APP-NVUE */
+		border-width: 0.5px;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		border-width: 1rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		/* #endif */
+		border-style: solid;
+		position: relative;
+		padding-left: 0;
+		padding-right: 0;
+		overflow: hidden;
+		/* #ifndef APP-NVUE */
+		transform: translateZ(0);
+		-webkit-touch-callout: none;
+		-webkit-user-select: none;
+		user-select: none;
+		/* #endif */
+	}
+
+	.tui-button__flex-1 {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+	}
+
+	.tui-button::after {
+		border: 0;
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-button__active {
+		overflow: hidden !important;
+	}
+
+	.tui-button__active::after {
+		content: ' ';
+		background-color: rgba(0, 0, 0, 0.1);
+		position: absolute;
+		width: 100%;
+		height: 100%;
+		left: 0;
+		right: 0;
+		top: 0;
+		transform: none;
+		z-index: 1;
+		border-radius: 0;
+	}
+
+	/* #endif */
+	.tui-button__text {
+		text-align: center;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center !important;
+		padding-left: 0 !important;
+
+	}
+
+	.tui-button__opacity {
+		opacity: 0.5;
+	}
+
+	.tui-text__bold {
+		font-weight: bold;
+	}
+</style>

+ 29 - 0
components/thorui/tui-form-field/tui-form-field.vue

@@ -0,0 +1,29 @@
+<template>
+	<view :class="{'tui-form__field':hidden}">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	//此组件只为form表单提交传递数据使用,暂时用于微信/百度小程序
+	export default {
+		name: "tui-form-field",
+		// #ifndef VUE3
+		behaviors: ['uni://form-field'],
+		// #endif
+		props: {
+			//是否为隐藏域
+			hidden: {
+				type: Boolean,
+				default: false
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-form__field {
+		display: none;
+		opacity: 0;
+	}
+</style>

+ 202 - 0
components/thorui/tui-form-item/tui-form-item.vue

@@ -0,0 +1,202 @@
+<template>
+	<view class="tui-form__item-wrap" :class="{'tui-form__highlight':highlight}"
+		:style="{padding:padding,background:background,marginTop:marginTop+'rpx',marginBottom:marginBottom+'rpx',borderRadius:radius}"
+		@tap="handleClick">
+		<!-- #ifdef APP-NVUE -->
+		<view class="tui-form__asterisk" v-if="asterisk">
+			<text :style="{color:asteriskColor}">*</text>
+		</view>
+		<!-- #endif -->
+		<!-- #ifndef APP-NVUE -->
+		<view class="tui-form__asterisk" v-if="asterisk" :style="{color:asteriskColor}">*</view>
+		<!-- #endif -->
+		<text :style="{width:labelWidth+'rpx',fontSize:labelSize+'rpx',color:labelColor,paddingRight:labelRight+'rpx'}"
+			v-if="label">{{label}}</text>
+		<view class="tui-form__item-content">
+			<slot></slot>
+		</view>
+		<slot name="right"></slot>
+		<view v-if="bottomBorder" :style="{background:borderColor,left:left+'rpx',right:right+'rpx'}"
+			class="tui-form__item-bottom"></view>
+		<view class="tui-form__item-arrow" v-if="arrow" :style="{'border-color':arrowColor}">
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tui-form-item',
+		emits: ['click'],
+		props: {
+			padding: {
+				type: String,
+				default: '26rpx 30rpx'
+			},
+			marginTop: {
+				type: [Number, String],
+				default: 0
+			},
+			marginBottom: {
+				type: [Number, String],
+				default: 0
+			},
+			label: {
+				type: String,
+				default: ''
+			},
+			labelSize: {
+				type: [Number, String],
+				default: 32
+			},
+			labelColor: {
+				type: String,
+				default: '#333'
+			},
+			labelWidth: {
+				type: [Number, String],
+				default: 140
+			},
+			labelRight: {
+				type: [Number, String],
+				default: 16
+			},
+			asterisk: {
+				type: Boolean,
+				default: false
+			},
+			asteriskColor: {
+				type: String,
+				default: '#EB0909'
+			},
+			background: {
+				type: String,
+				default: '#fff'
+			},
+			highlight: {
+				type: Boolean,
+				default: false
+			},
+			arrow: {
+				type: Boolean,
+				default: false
+			},
+			arrowColor: {
+				type: String,
+				default: '#c0c0c0'
+			},
+			bottomBorder: {
+				type: Boolean,
+				default: true
+			},
+			borderColor: {
+				type: String,
+				default: '#eaeef1'
+			},
+			left: {
+				type: [Number, String],
+				default: 30
+			},
+			right: {
+				type: [Number, String],
+				default: 0
+			},
+			radius: {
+				type: String,
+				default: '0'
+			},
+			index: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {
+					index: this.index
+				});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-form__item-wrap {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		box-sizing: border-box;
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		flex: 1;
+		align-items: center;
+		position: relative;
+	}
+
+	.tui-form__highlight:active {
+		background-color: #f1f1f1 !important;
+	}
+
+	.tui-form__asterisk {
+		position: absolute;
+		left: 12rpx;
+		/* #ifndef APP-NVUE */
+		height: 30rpx;
+		top: 50%;
+		transform: translateY(-50%);
+		line-height: 1.15;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		align-items: center;
+		justify-content: center;
+		line-height: 1;
+		/* #endif */
+	}
+
+	.tui-form__item-label {
+		padding-right: 12rpx;
+		/* #ifndef APP-NVUE */
+		display: inline-block;
+		flex-shrink: 0;
+		/* #endif */
+	}
+
+	.tui-form__item-content {
+		flex: 1;
+	}
+
+	.tui-form__item-bottom {
+		position: absolute;
+		bottom: 0;
+		/* #ifdef APP-NVUE */
+		height: 0.5px;
+		z-index: -1;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		height: 1px;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 100%;
+		z-index: 1;
+		/* #endif */
+	}
+
+	.tui-form__item-arrow {
+		height: 40rpx;
+		width: 40rpx;
+		border-width: 3px 3px 0 0;
+		border-style: solid;
+		transform: rotate(45deg) scale(0.5);
+		/* #ifndef APP-NVUE */
+		border-radius: 4rpx;
+		flex-shrink: 0;
+		margin-left: auto;
+		box-sizing: border-box;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		border-top-right-radius: 3rpx;
+		/* #endif */
+		transform-origin: center center;
+		margin-right: -5.8579rpx;
+	}
+</style>

+ 200 - 0
components/thorui/tui-form/tui-form.vue

@@ -0,0 +1,200 @@
+<template>
+	<view class="tui-form__box" :style="{backgroundColor:backgroundColor,padding:padding,borderRadius:radius}">
+		<slot></slot>
+		<view class="tui-form__errmsg"
+			:style="{top:tipTop+'px',padding:tipPadding,backgroundColor:tipBackgroundColor,borderRadius:tipRidus}"
+			v-if="showMessage" :class="{'tui-message__show':errorMsg}"><text class="tui-form__text"
+				:style="{fontSize:tipSize+'rpx',color:tipColor}">{{errorMsg}}</text></view>
+		<view class="tui-form__mask" v-if="disabled"></view>
+	</view>
+</template>
+
+<script>
+	import form from "./tui-validation.js"
+	export default {
+		name: "tui-form",
+		props: {
+			//表单数据对象,即将废弃
+			model: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			//表单验证规则,即将废弃
+			rules: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//表单背景颜色
+			backgroundColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//表单padding值
+			padding: {
+				type: String,
+				default: '0'
+			},
+			//是否显示校验错误信息
+			showMessage: {
+				type: Boolean,
+				default: true
+			},
+			//表单圆角值
+			radius: {
+				type: String,
+				default: '0'
+			},
+			//是否禁用该表单内的所有组件,透明遮罩层
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//提示框top值 px
+			tipTop: {
+				type: Number
+					// #ifdef H5
+					,
+				default: 44
+					// #endif
+					// #ifndef H5
+					,
+				default: 0
+				// #endif
+			},
+			//错误提示框padding值
+			tipPadding: {
+				type: String,
+				default: '20rpx'
+			},
+			//错误提示框背景色
+			tipBackgroundColor: {
+				type: String,
+				default: '#f74d54'
+			},
+			//错误提示字体大小
+			tipSize: {
+				type: Number,
+				default: 28
+			},
+			//错误提示字体颜色
+			tipColor: {
+				type: String,
+				default: '#fff'
+			},
+			//错误提示框圆角值
+			tipRidus: {
+				type: String,
+				default: '12rpx'
+			},
+			//错误消息显示时间 ms
+			duration: {
+				type: Number,
+				default: 2000
+			}
+		},
+		data() {
+			return {
+				errorMsg: '',
+				timer: null
+			};
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.clearTimer()
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.clearTimer()
+		},
+		// #endif
+		methods: {
+			clearTimer() {
+				clearTimeout(this.timer)
+				this.timer = null;
+			},
+			//{Object} model 表单数据对象
+			//{Array} rules 表单验证规则
+			validate(model, rules) {
+				model = model || this.model
+				rules = rules || this.rules
+				return new Promise((resolve, reject) => {
+					let checkRes = form.validation(model, rules);
+					let obj = {
+						isPass: true,
+						errorMsg: checkRes
+					};
+					if (!checkRes) {
+						resolve(obj)
+					} else {
+						if (this.showMessage) {
+							this.clearTimer()
+							this.errorMsg = checkRes;
+							this.timer = setTimeout(() => {
+								this.errorMsg = ''
+							}, this.duration)
+						}
+						obj.isPass = false;
+						reject(obj)
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-form__box {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		box-sizing: border-box;
+		/* #endif */
+		flex: 1;
+		position: relative;
+	}
+
+	.tui-form__errmsg {
+		position: fixed;
+		z-index: 900;
+		text-align: center;
+		left: 20rpx;
+		right: 20rpx;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		display: flex;
+		word-break: break-all;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+		padding: 24rpx;
+		opacity: 0;
+		transform: translateZ(0) translateY(-100%);
+		transition-property: transform, opacity;
+		transition-duration: 0.25s;
+		transition-delay: 0s;
+		transition-timing-function: ease-in-out;
+	}
+
+	.tui-form__text {
+		text-align: center;
+	}
+
+	.tui-message__show {
+		transform: translateY(0) translateZ(0);
+		opacity: 1;
+	}
+
+	.tui-form__mask {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0);
+		z-index: 99;
+	}
+</style>

+ 313 - 0
components/thorui/tui-form/tui-validation.js

@@ -0,0 +1,313 @@
+/**
+ * 表单验证
+ * @author echo.
+ * @version 1.6.6
+ **/
+
+const form = {
+	//非必填情况下,如果值为空,则不进行校验
+	//当出现错误时返回错误消息,否则返回空即为验证通过
+	/*
+	 formData:Object 表单对象。{key:value,key:value},key==rules.name
+	 rules: Array [{name:name,rule:[],msg:[],validator:[],{name:name,rule:[],msg:[],validator:[]}]
+			name:name 属性=> 元素的名称
+			rule:字符串数组 ["required","isMobile","isEmail","isCarNo","isIdCard","isAmount","isNum","isChinese","isNotChinese","isEnglish",isEnAndNo","isSpecial","isEmoji",""isDate","isUrl","isSame:key","range:[1,9]","minLength:9","maxLength:Number","isKeyword:key1,key2,key3..."]
+			msg:数组 []。 与数组 rule 长度相同,对应的错误提示信息
+			validator:[{msg:'错误消息',method:Function}],自定义验证方法组,函数约定:(value)=>{ return true or false}
+	*/
+	validation: function(formData, rules) {
+		for (let item of rules) {
+			let key = item.name;
+			let rule = item.rule;
+			let validator = item.validator;
+			let msgArr = item.msg;
+			if (!key || !rule || rule.length === 0 || !msgArr || msgArr.length === 0 || (!~rule.indexOf(
+						"required") && formData[key].toString()
+					.length === 0)) {
+				continue;
+			}
+			for (let i = 0, length = rule.length; i < length; i++) {
+				let ruleItem = rule[i];
+				let msg = msgArr[i];
+				if (!msg || !ruleItem) 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 "isNotChinese":
+						isError = !form._isNotChinese(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;
+					case "isKeyword":
+						isError = !form._isKeyword(formData[key], value)
+						break;
+					default:
+						break;
+				}
+
+				if (isError) {
+					return msg;
+				}
+			}
+			if (validator && validator.length > 0) {
+				for (let model of validator) {
+					let func = model.method;
+					if (func && !func(formData[key])) {
+						return model.msg;
+					}
+				}
+			}
+		}
+		return "";
+	},
+	//允许填写字符串null或者undefined
+	_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)
+	},
+	//是否不包含中文,可以有特殊字符
+	_isNotChinese: function(value) {
+		let reg = /.*[\u4e00-\u9fa5]+.*$/;
+		let result = true;
+		if (reg.test(value)) {
+			result = false
+		}
+		return result
+	},
+	_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})(:[0-9]{1,5})?((\/?)|(\/[\\\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)
+	},
+	_isKeyword: function(value, keywords) {
+		//是否包含关键词,敏感词,多个以英文逗号分隔,包含则为false,弹出提示语!
+		let result = true;
+		if (!keywords) return result;
+		let key = keywords.split(',');
+		for (let i = 0, len = key.length; i < len; i++) {
+			if (~value.indexOf(key[i])) {
+				result = false;
+				break;
+			}
+		}
+		return result;
+	}
+};
+export default {
+	validation: form.validation
+};

+ 204 - 0
components/thorui/tui-gallery/tui-gallery.vue

@@ -0,0 +1,204 @@
+<template>
+	<view class="tui-gallery" :class="{'tui-gallery_show':show}" @tap="hideGallery">
+		<view class="tui-gallery__info">{{currentIndex+1}}/{{getLen}}</view>
+		<swiper class="tui-gallery__img__wrap" :indicator-dots="false" @change="change" :current="defCurIndex"
+			:autoplay="false" :duration="500">
+			<swiper-item v-for="(item,index) in imgUrls" :key="index">
+				<image mode="aspectFit" class="tui-gallery__img" :src="item.src"></image>
+			</swiper-item>
+		</swiper>
+		<view class="tui-gallery__desc" v-if="!showDelete">
+			{{getDesc(currentIndex,imgUrls)}}
+		</view>
+		<view class="tui-gallery__operate" hover-class="tui-opacity__del" :hover-start-time="150" @tap.stop="deleteImg"
+			v-if="showDelete">
+			删除
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiGallery',
+		emits: ['change', 'delete', 'hide'],
+		props: {
+			urls: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			showDelete: {
+				type: Boolean,
+				default: false
+			},
+			show: {
+				type: Boolean,
+				default: false
+			},
+			current: {
+				type: Number,
+				default: 0
+			},
+			hideOnClick: {
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			getLen() {
+				return this.imgUrls.length
+			}
+		},
+		watch: {
+			urls(newVal, oldVal) {
+				this.imgUrls = newVal
+			},
+			current(newVal) {
+				this.defCurIndex = this.currentIndex;
+				let val = Number(newVal)
+				setTimeout(() => {
+					this.defCurIndex = val;
+					this.currentIndex = val;
+				}, 20)
+			}
+		},
+		mounted() {
+			this.defCurIndex = Number(this.current);
+			this.currentIndex = this.defCurIndex;
+			this.imgUrls = this.urls;
+		},
+		data() {
+			return {
+				imgUrls: [],
+				currentIndex: 0,
+				defCurIndex: 0
+			};
+		},
+		methods: {
+			getDesc(index, imgUrls) {
+				let desc = ''
+				let item = imgUrls[index]
+				if (item) {
+					desc = item.desc
+				}
+				return desc
+			},
+			change(e) {
+				this.currentIndex = e.detail.current
+				this.$emit('change', {
+					current: e.detail.current
+				});
+			},
+			deleteImg() {
+				const imgs = this.imgUrls;
+				const url = imgs.splice(this.current, 1);
+				this.$emit('delete', {
+					url: url[0],
+					index: this.current
+				});
+
+				if (imgs.length === 0) {
+					this.hideGallery();
+					return;
+				}
+
+				this.current = 0;
+				this.imgUrls = imgs
+			},
+			hideGallery() {
+				if (this.hideOnClick) {
+					this.$emit('hide', {});
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-gallery {
+		position: fixed;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		background-color: #000;
+		z-index: 1000;
+		display: none;
+	}
+
+	.tui-gallery__img,
+	.tui-gallery__operate,
+	.tui-gallery__desc {
+		position: absolute;
+		left: 0;
+		left: constant(safe-area-inset-left);
+		left: env(safe-area-inset-left);
+		right: 0;
+		right: constant(safe-area-inset-right);
+		right: env(safe-area-inset-right)
+	}
+
+	.tui-gallery__img {
+		width: 100%;
+		height: 100%;
+		top: 0;
+		top: constant(safe-area-inset-top);
+		top: env(safe-area-inset-top);
+		bottom: 60px;
+		bottom: calc(60px + constant(safe-area-inset-bottom));
+		bottom: calc(60px + env(safe-area-inset-bottom));
+		background: 50% no-repeat;
+		background-size: contain
+	}
+
+	.tui-gallery__operate,
+	.tui-gallery__desc {
+		position: absolute;
+		bottom: 0;
+		padding-bottom: 0;
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+		background-color: #0d0d0d;
+		color: #fff;
+		line-height: 60px;
+		text-align: center;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		padding: 0 30rpx;
+		box-sizing: border-box;
+		z-index: 10;
+	}
+
+
+	.tui-gallery__info {
+		color: #fff;
+		font-size: 17px;
+		line-height: 60px;
+		min-height: 60px;
+		text-align: center
+	}
+
+	.tui-gallery__img__wrap {
+		-webkit-box-flex: 1;
+		-webkit-flex: 1;
+		flex: 1;
+		position: relative;
+		font-size: 0
+	}
+
+	.tui-gallery__operate {
+		position: static
+	}
+
+	.tui-gallery_show {
+		display: flex !important;
+		flex-direction: column !important;
+		flex-wrap: nowrap !important;
+	}
+
+	.tui-opacity__del {
+		opacity: 0.5;
+	}
+</style>

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

@@ -0,0 +1,153 @@
+<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",
+		emits: ['click'],
+		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;
+	}
+	/* #ifdef MP-BAIDU */
+	.tui-grid:active{
+		background-color: #f7f7f9;
+	}
+	/* #endif */
+
+	.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>

+ 190 - 0
components/thorui/tui-icon/tui-icon.js

@@ -0,0 +1,190 @@
+export default {
+	"about": "\ue772",
+	"about-fill": "\ue771",
+	"add": "\ue770",
+	"add-fill": "\ue76f",
+	"addmessage": "\ue76e",
+	"addressbook": "\ue76d",
+	"agree": "\ue76c",
+	"agree-fill": "\ue76b",
+	"alarm": "\ue76a",
+	"alarm-fill": "\ue769",
+	"alipay": "\ue768",
+	"android": "\ue767",
+	"applets": "\ue766",
+	"arrowdown": "\ue765",
+	"arrowleft": "\ue764",
+	"arrowright": "\ue763",
+	"arrowup": "\ue762",
+	"attestation": "\ue761",
+	"back": "\ue760",
+	"bag": "\ue75f",
+	"bag-fill": "\ue75e",
+	"balloon": "\ue75d",
+	"bankcard": "\ue75c",
+	"bankcard-fill": "\ue75b",
+	"bottom": "\ue75a",
+	"calendar": "\ue759",
+	"camera": "\ue758",
+	"camera-fill": "\ue757",
+	"camera-add": "\ue756",
+	"card": "\ue755",
+	"card-fill": "\ue754",
+	"cart": "\ue753",
+	"cart-fill": "\ue752",
+	"category": "\ue751",
+	"category-fill": "\ue750",
+	"check": "\ue74f",
+	"circle": "\ue74e",
+	"circle-fill": "\ue74d",
+	"circle-selected": "\ue74c",
+	"clock": "\ue74b",
+	"clock-fill": "\ue74a",
+	"close": "\ue749",
+	"close-fill": "\ue748",
+	"community": "\ue747",
+	"community-fill": "\ue746",
+	"computer": "\ue745",
+	"computer-fill": "\ue744",
+	"coupon": "\ue743",
+	"delete": "\ue742",
+	"deletekey": "\ue741",
+	"dingtalk": "\ue740",
+	"dissatisfied": "\ue73f",
+	"down": "\ue73e",
+	"download": "\ue73d",
+	"edit": "\ue73c",
+	"ellipsis": "\ue73b",
+	"enlarge": "\ue73a",
+	"evaluate": "\ue739",
+	"exchange": "\ue738",
+	"explain": "\ue737",
+	"explain-fill": "\ue736",
+	"explore": "\ue735",
+	"explore-fill": "\ue734",
+	"eye": "\ue733",
+	"feedback": "\ue732",
+	"fingerprint": "\ue730",
+	"friendadd": "\ue72f",
+	"friendadd-fill": "\ue72e",
+	"gps": "\ue72d",
+	"histogram": "\ue72c",
+	"home": "\ue72b",
+	"home-fill": "\ue72a",
+	"house": "\ue729",
+	"imface": "\ue728",
+	"imkeyboard": "\ue727",
+	"immore": "\ue726",
+	"imvoice": "\ue725",
+	"ios": "\ue724",
+	"kefu": "\ue723",
+	"label": "\ue722",
+	"label-fill": "\ue721",
+	"like": "\ue720",
+	"like-fill": "\ue71f",
+	"link": "\ue71e",
+	"listview": "\ue71d",
+	"loading": "\ue71c",
+	"location": "\ue71b",
+	"mail": "\ue71a",
+	"mail-fill": "\ue719",
+	"manage": "\ue718",
+	"manage-fill": "\ue717",
+	"member": "\ue716",
+	"member-fill": "\ue715",
+	"message": "\ue714",
+	"message-fill": "\ue713",
+	"mobile": "\ue712",
+	"moments": "\ue711",
+	"more": "\ue710",
+	"more-fill": "\ue70f",
+	"narrow": "\ue70e",
+	"news": "\ue70d",
+	"news-fill": "\ue70c",
+	"nodata": "\ue70b",
+	"notice": "\ue699",
+	"notice-fill": "\ue698",
+	"offline": "\ue697",
+	"offline-fill": "\ue696",
+	"oppose": "\ue695",
+	"oppose-fill": "\ue694",
+	"order": "\ue693",
+	"partake": "\ue692",
+	"people": "\ue691",
+	"people-fill": "\ue690",
+	"pic": "\ue68f",
+	"pic-fill": "\ue68e",
+	"picture": "\ue68d",
+	"pie": "\ue68c",
+	"plus": "\ue689",
+	"polygonal": "\ue688",
+	"position": "\ue686",
+	"pwd": "\ue685",
+	"qq": "\ue684",
+	"qrcode": "\ue682",
+	"redpacket": "\ue681",
+	"redpacket-fill": "\ue680",
+	"reduce": "\ue67f",
+	"refresh": "\ue67e",
+	"revoke": "\ue67d",
+	"satisfied": "\ue67c",
+	"screen": "\ue67b",
+	"search": "\ue67a",
+	"search-2": "\ue679",
+	"send": "\ue678",
+	"service": "\ue677",
+	"service-fill": "\ue676",
+	"setup": "\ue675",
+	"setup-fill": "\ue674",
+	"share": "\ue673",
+	"share-fill": "\ue672",
+	"shield": "\ue671",
+	"shop": "\ue670",
+	"shop-fill": "\ue66f",
+	"shut": "\ue66e",
+	"signin": "\ue66d",
+	"sina": "\ue66c",
+	"skin": "\ue66b",
+	"soso": "\ue669",
+	"square": "\ue668",
+	"square-fill": "\ue667",
+	"square-selected": "\ue666",
+	"star": "\ue665",
+	"star-fill": "\ue664",
+	"strategy": "\ue663",
+	"sweep": "\ue662",
+	"time": "\ue661",
+	"time-fill": "\ue660",
+	"todown": "\ue65f",
+	"toleft": "\ue65e",
+	"tool": "\ue65d",
+	"top": "\ue65c",
+	"toright": "\ue65b",
+	"towardsleft": "\ue65a",
+	"towardsright": "\ue659",
+	"towardsright-fill": "\ue658",
+	"transport": "\ue657",
+	"transport-fill": "\ue656",
+	"turningdown": "\ue654",
+	"turningleft": "\ue653",
+	"turningright": "\ue652",
+	"turningup": "\ue651",
+	"unreceive": "\ue650",
+	"seen": "\ue7d2",
+	"unseen": "\ue7d1",
+	"up": "\ue64e",
+	"upload": "\ue64c",
+	"video": "\ue64b",
+	"voice": "\ue649",
+	"voice-fill": "\ue648",
+	"voipphone": "\ue647",
+	"wallet": "\ue646",
+	"warning": "\ue645",
+	"wealth": "\ue644",
+	"wealth-fill": "\ue643",
+	"weather": "\ue642",
+	"wechat": "\ue641",
+	"wifi": "\ue640",
+	"play": "\ue7d5",
+	"suspend": "\ue7d4"
+}

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


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

@@ -0,0 +1,1091 @@
+<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" :class="{'tui-cropper__image-hidden':!imageUrl}" :src="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',
+		emits: ['ready', 'cropper', 'imageLoad'],
+		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
+			},
+			//生成的图片尺寸相对剪裁框的比例
+			// #ifndef MP-QQ
+			scaleRatio: {
+				type: [Number, String],
+				default: 2
+			},
+			// #endif
+			// #ifdef MP-QQ
+			scaleRatio: {
+				type: [Number, String],
+				default: 3
+			},
+			// #endif
+			//图片的质量,取值范围为 (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'
+			},
+			//裁剪后图片类型:jpg/png
+			fileType: {
+				type: String,
+				default: '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),
+							// #ifdef MP-QQ
+							destWidth: this.canvasWidth * this.scaleRatio * 2,
+							destHeight: Math.round(this.canvasHeight) * this.scaleRatio * 2,
+							// #endif
+
+							// #ifndef MP-QQ
+							destWidth: this.canvasWidth * this.scaleRatio,
+							destHeight: Math.round(this.canvasHeight) * this.scaleRatio,
+							// #endif
+
+							fileType: this.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.ctx.rotate(((360 - this.angle % 360) * Math
+									.PI) / 180);
+								this.ctx.translate(-xpos * this.scaleRatio, -
+									ypos * this.scaleRatio);
+								this.ctx.clearRect(0, 0, this.canvasWidth * this
+									.scaleRatio, this.canvasHeight * this.scaleRatio);
+								this.ctx.draw();
+								this.$emit('cropper', data);
+							});
+						} else {
+							this.ctx.toTempFilePath({
+								...params,
+								success: res => {
+									data.url = res.apFilePath;
+									this.loadding && uni.hideLoading();
+									this.ctx.rotate(((360 - this.angle % 360) * Math
+										.PI) / 180);
+									this.ctx.translate(-xpos * this.scaleRatio, -
+										ypos * this.scaleRatio);
+									this.ctx.clearRect(0, 0, this.canvasWidth * this
+										.scaleRatio, this.canvasHeight * this.scaleRatio);
+									this.ctx.draw();
+									this.$emit('cropper', data);
+								}
+							});
+						}
+						// #endif
+
+						// #ifndef MP-ALIPAY
+						let isBase64=this.isBase64
+						// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+						isBase64 = false;
+						// #endif
+						if (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({
+					// #ifndef MP-ALIPAY
+					mask: true,
+					// #endif
+					title: '请稍候...'
+				});
+			},
+			stop() {},
+			back() {
+				uni.navigateBack();
+			},
+			setAngle() {
+				this.cutAnimation = true;
+				this.angle = this.angle + 90;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-container {
+		width: 100vw;
+		height: 100vh;
+		padding: 0;
+		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;
+		padding: 0;
+		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__image-hidden {
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.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>

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

@@ -0,0 +1,164 @@
+<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></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiImageGroup',
+	emits: ['errorEvent','loaded','click'],
+	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>

+ 621 - 0
components/thorui/tui-index-list/tui-index-list.vue

@@ -0,0 +1,621 @@
+<template>
+	<view class="tui-index-list">
+		<scroll-view class="tui-scroll__view" :style="{ height: getHeight }" scroll-y :scroll-top="scrollTop"
+			@scroll="scroll">
+			<slot name="header"></slot>
+			<view class="tui-content__box">
+				<view class="tui-item__select" v-for="(item, index) in listData" :key="index">
+					<view v-if="index == listItemCur" class="tui-content__title"
+						:class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
+						<view class="tui-title__item"
+							:style="{ background: background_cur, color: color_cur, fontSize: size, height: height, padding: padding }">
+							{{ item.letter }}
+						</view>
+					</view>
+					<view v-else-if="index == listItemCur + 1" class="tui-content__title"
+						:class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
+						<view class="tui-title__item"
+							:style="{ background: background_next, color: color_next, fontSize: size, height: height, padding: padding }">
+							{{ item.letter }}
+						</view>
+					</view>
+					<view v-else class="tui-content__title"
+						:class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
+						<view class="tui-title__item"
+							:style="{ background: background, color: color, fontSize: size, height: height, padding: padding }">
+							{{ item.letter }}
+						</view>
+					</view>
+					<slot name="item" :entity="item.data" :index="index"></slot>
+				</view>
+			</view>
+			<slot name="footer"></slot>
+		</scroll-view>
+		<view class="tui-index__indicator"
+			:class="[touching && indicatorTop != -1 ? 'tui-indicator__show' : '', treeKeyTran ? 'tui-indicator__tran' : '']"
+			:style="{ top: indicatorTop + 'px' }">
+			{{ listData[treeItemCur] && listData[treeItemCur].letter }}
+		</view>
+		<view id="tui_index__letter" class="tui-index__letter" @touchstart.stop="touchStart"
+			@touchmove.stop.prevent="touchMove" @touchend.stop="touchEnd" @touchcancel.stop="touchEnd">
+			<view class="tui-letter__item" :class="[index === treeItemCur ? 'tui-letter__cur' : '']"
+				v-for="(item, index) in listData" :key="index" @tap="letterClick(index,item.letter)">
+				<view class="tui-letter__key"
+					:style="{ background: index === treeItemCur ? activeKeyBackground : '', color: index === treeItemCur ? activeKeyColor : keyColor }">
+					{{ item.letter }}
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	let ColorUtil = {
+		rgbToHex(r, g, b) {
+			let hex = ((r << 16) | (g << 8) | b).toString(16);
+			return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex;
+		},
+		hexToRgb(hex) {
+			let rgb = [];
+			if (hex.length === 4) {
+				let text = hex.substring(1, 4);
+				hex = '#' + text + text;
+			}
+			for (let i = 1; i < 7; i += 2) {
+				rgb.push(parseInt('0x' + hex.slice(i, i + 2)));
+			}
+			return rgb;
+		},
+		/**
+		 * 生成渐变过渡色数组 {startColor: 开始颜色值, endColor: 结束颜色值, step: 生成色值数组长度}
+		 */
+		gradient(startColor, endColor, step) {
+			// 将hex转换为rgb
+			let sColor = this.hexToRgb(startColor),
+				eColor = this.hexToRgb(endColor);
+
+			// 计算R\G\B每一步的差值
+			let rStep = (eColor[0] - sColor[0]) / step,
+				gStep = (eColor[1] - sColor[1]) / step,
+				bStep = (eColor[2] - sColor[2]) / step;
+
+			let gradientColorArr = [];
+			for (let i = 0; i < step; i++) {
+				// 计算每一步的hex值
+				gradientColorArr.push(this.rgbToHex(parseInt(rStep * i + sColor[0]), parseInt(gStep * i + sColor[1]),
+					parseInt(bStep * i + sColor[2])));
+			}
+			return gradientColorArr;
+		},
+		/**
+		 * 生成随机颜色值
+		 */
+		generateColor() {
+			let color = '#';
+			for (let i = 0; i < 6; i++) {
+				color += ((Math.random() * 16) | 0).toString(16);
+			}
+			return color;
+		}
+	};
+	export default {
+		name: 'tuiIndexList',
+		emits: ['letterClick'],
+		props: {
+			// 数据源
+			listData: {
+				type: Array,
+				default () {
+					return [];
+				}
+			},
+			// 顶部高度
+			top: {
+				type: Number,
+				default: 0
+			},
+			// 底部高度
+			bottom: {
+				type: Number,
+				default: 0
+			},
+			//top和bottom单位,可传rpx 或 px
+			unit: {
+				type: String,
+				default: 'px'
+			},
+			//sticky letter 是否显示上边线条
+			topLine: {
+				type: Boolean,
+				default: true
+			},
+			//sticky letter 是否显示下边线条
+			bottomLine: {
+				type: Boolean,
+				default: true
+			},
+			height: {
+				type: String,
+				default: '60rpx'
+			},
+			color: {
+				type: String,
+				default: '#666'
+			},
+			activeColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			size: {
+				type: String,
+				default: '26rpx'
+			},
+			background: {
+				type: String,
+				default: '#ededed'
+			},
+			activeBackground: {
+				type: String,
+				default: '#FFFFFF'
+			},
+			padding: {
+				type: String,
+				default: '0 20rpx'
+			},
+			keyColor: {
+				type: String,
+				default: '#666'
+			},
+			activeKeyColor: {
+				type: String,
+				default: '#FFFFFF'
+			},
+			activeKeyBackground: {
+				type: String,
+				default: '#5677fc'
+			},
+			//重新初始化[可异步加载时使用,设置大于0的数]
+			reinit: {
+				type: Number,
+				default: 0
+			}
+		},
+		computed: {
+			getHeight() {
+				return `calc(100vh - ${this.top + this.bottom + this.unit})`;
+			},
+			getChange() {
+				return `${this.top}-${this.bottom}-${this.reinit}`;
+			}
+		},
+		watch: {
+			listData(val) {
+				this.init();
+			},
+			getChange(val) {
+				this.init();
+			}
+		},
+		data() {
+			return {
+				remScale: 1, // 缩放比例
+				realTop: 0, // 计算后顶部高度实际值
+				realBottom: 0, // 计算后底部高度实际值
+				treeInfo: {
+					// 索引树节点信息
+					treeTop: 0,
+					treeBottom: 0,
+					itemHeight: 0,
+					itemMount: 0
+				},
+				indicatorTopList: [], // 指示器节点信息列表
+				maxScrollTop: 0, // 最大滚动高度
+				blocks: [], // 节点组信息
+
+				/* 渲染数据 */
+				treeItemCur: -1, // 索引树的聚焦项
+				listItemCur: -1, // 节点树的聚焦项
+				touching: false, // 是否在触摸索引树中
+				scrollTop: 0, // 节点树滚动高度
+				indicatorTop: -1, // 指示器顶部距离
+				treeKeyTran: false,
+				background_cur: '',
+				color_cur: '',
+				background_next: '',
+				color_next: '',
+				colors: [],
+				backgroundColors: []
+			};
+		},
+		methods: {
+			scroll(e) {
+				if (this.touching) return;
+
+				let scrollTop = e.detail.scrollTop;
+				if (scrollTop > this.maxScrollTop) return;
+
+				let blocks = this.blocks,
+					stickyTitleHeight = this.remScale * 30;
+				let len = blocks.length - 1;
+				this.background_cur = this.background;
+				this.color_cur = this.color;
+				for (let i = len; i >= 0; i--) {
+					let block = blocks[i];
+					// 判断当前滚动值 scrollTop 所在区间, 以得到当前聚焦项
+					if (scrollTop >= block.itemTop && scrollTop < block.itemBottom) {
+						// 判断当前滚动值 scrollTop 是否在当前聚焦项底一个 .block__title 高度范围内, 如果是则开启过度色值计算
+						if (scrollTop > block.itemBottom - stickyTitleHeight) {
+							let percent = Math.floor(((scrollTop - (block.itemBottom - stickyTitleHeight)) /
+								stickyTitleHeight) * 100);
+							this.background_cur = this.backgroundColors[percent];
+							this.color_cur = this.colors[percent];
+							this.background_next = this.backgroundColors[100 - percent];
+							this.color_next = this.colors[100 - percent];
+							this.treeItemCur = i;
+							this.listItemCur = i;
+						} else if (scrollTop <= block.itemBottom - stickyTitleHeight) {
+							this.background_cur = this.activeBackground;
+							this.color_cur = this.activeColor;
+							this.background_next = this.background;
+							this.color_next = this.color;
+							this.treeItemCur = i;
+							this.listItemCur = i;
+						}
+						break;
+					}
+				}
+			},
+			/**
+			 * tree 触摸开始
+			 */
+			touchStart(e) {
+				// 获取触摸点信息
+				let startTouch = e.changedTouches[0];
+				if (!startTouch) return;
+				this.touching = true;
+				let treeItemCur = this.getCurrentTreeItem(startTouch.pageY);
+				this.setValue(treeItemCur);
+			},
+			/**
+			 * tree 触摸移动
+			 */
+			touchMove(e) {
+				// 获取触摸点信息
+				let currentTouch = e.changedTouches[0];
+				if (!currentTouch) return;
+
+				// 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
+				if (!this.touching) {
+					this.touching = true;
+				}
+
+				let treeItemCur = this.getCurrentTreeItem(currentTouch.pageY);
+				this.setValue(treeItemCur);
+			},
+			/**
+			 * tree 触摸结束
+			 */
+			touchEnd(e) {
+				let treeItemCur = this.treeItemCur;
+				let listItemCur = this.listItemCur;
+				if (treeItemCur !== listItemCur) {
+					this.treeItemCur = listItemCur;
+					this.indicatorTop = this.indicatorTopList[treeItemCur];
+				}
+				this.treeKeyTran = true;
+				setTimeout(() => {
+					this.touching = false;
+					this.treeKeyTran = false;
+				}, 300);
+			},
+			letterClick(index, letter) {
+				// #ifdef H5
+				this.setValue(index);
+				this.touchEnd()
+				// #endif
+				this.$emit('letterClick', {
+					index: index,
+					letter: letter
+				})
+			},
+			/**
+			 * 获取当前触摸的 tree-item
+			 * @param pageY: 当前触摸点pageY
+			 */
+			getCurrentTreeItem(pageY) {
+				let {
+					treeTop,
+					treeBottom,
+					itemHeight,
+					itemMount
+				} = this.treeInfo;
+
+				if (pageY < treeTop) {
+					return 0;
+				} else if (pageY >= treeBottom) {
+					return itemMount - 1;
+				} else {
+					return Math.floor((pageY - treeTop) / itemHeight);
+				}
+			},
+			/**
+			 * 触摸之后后设置对应value
+			 */
+			setValue(treeItemCur) {
+				if (treeItemCur === this.treeItemCur) return;
+
+				let block = this.blocks[treeItemCur];
+				if (!block) return;
+
+				let {
+					scrollTop,
+					scrollIndex
+				} = block,
+				indicatorTop = this.indicatorTopList[treeItemCur];
+
+				this.background_cur = this.activeBackground;
+				this.color_cur = this.activeColor;
+				this.background_next = this.background;
+				this.color_next = this.color;
+				this.treeItemCur = treeItemCur;
+				this.scrollTop = scrollTop;
+				this.listItemCur = scrollIndex;
+				this.indicatorTop = indicatorTop;
+			},
+			/**
+			 * 清除参数
+			 */
+			clearData() {
+				this.treeItemCur = 0; // 索引树的聚焦项
+				this.listItemCur = 0; // 节点树的聚焦项
+				this.touching = false; // 是否在触摸索引树中
+				this.scrollTop = 0; // 节点树滚动高度
+				this.indicatorTop = -1; // 指示器顶部距离
+				this.treeKeyTran = false;
+				this.background_cur = this.background;
+				this.color_cur = this.color;
+				this.background_next = this.background;
+				this.color_next = this.color;
+			},
+			/**
+			 *  初始化获取 dom 信息
+			 */
+			initDom() {
+				let {
+					windowHeight,
+					windowWidth
+				} = uni.getSystemInfoSync();
+				let remScale = (windowWidth || 375) / 375,
+					realTop = (this.top * remScale) / 2,
+					realBottom = (this.bottom * remScale) / 2,
+					colors = ColorUtil.gradient(this.activeColor, this.color, 100),
+					backgroundColors = ColorUtil.gradient(this.activeBackground, this.background, 100);
+
+				this.remScale = remScale;
+				this.realTop = realTop;
+				this.realBottom = realBottom;
+				this.colors = colors;
+				this.backgroundColors = backgroundColors;
+
+				uni.createSelectorQuery()
+					.in(this)
+					.select('#tui_index__letter')
+					.boundingClientRect(res => {
+						let treeTop = res.top,
+							treeBottom = res.top + res.height,
+							itemHeight = res.height / this.listData.length,
+							itemMount = this.listData.length;
+
+						let indicatorTopList = this.listData.map((item, index) => {
+							return itemHeight / 2 + index * itemHeight + treeTop - remScale * 25;
+						});
+						this.treeInfo = {
+							treeTop: treeTop,
+							treeBottom: treeBottom,
+							itemHeight: itemHeight,
+							itemMount: itemMount
+						};
+						this.indicatorTopList = indicatorTopList;
+					})
+					.exec();
+
+				uni.createSelectorQuery()
+					.in(this)
+					.select('.tui-content__box')
+					.boundingClientRect(res => {
+						let maxScrollTop = res.height - (windowHeight - realTop - realBottom);
+
+						uni.createSelectorQuery()
+							.in(this)
+							.selectAll('.tui-item__select')
+							.boundingClientRect(res => {
+								let maxScrollIndex = -1;
+
+								let blocks = res.map((item, index) => {
+									// Math.ceil 向上取整, 防止索引树切换列表时候造成真机固定头部上边线显示过粗问题
+									let itemTop = Math.ceil(item.top - realTop),
+										itemBottom = Math.ceil(itemTop + item.height);
+
+									if (maxScrollTop >= itemTop && maxScrollTop < itemBottom)
+										maxScrollIndex = index;
+
+									return {
+										itemTop: itemTop,
+										itemBottom: itemBottom,
+										scrollTop: itemTop >= maxScrollTop ? maxScrollTop : itemTop,
+										scrollIndex: maxScrollIndex === -1 ? index : maxScrollIndex
+									};
+								});
+
+								this.maxScrollTop = maxScrollTop;
+								this.blocks = blocks;
+							})
+							.exec();
+					})
+					.exec();
+			},
+			/**
+			 *  初始化
+			 */
+			init() {
+				this.clearData();
+				// 避免获取不到节点信息报错问题
+				if (this.listData.length === 0) {
+					return;
+				}
+				// 异步加载数据时候, 延迟执行 initDom 方法
+				setTimeout(() => this.initDom(), 1200);
+			}
+		},
+		mounted() {
+			this.init();
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-index-list {
+		width: 100vw;
+		overflow: hidden;
+		position: relative;
+	}
+
+	.tui-scroll__view {
+		width: 100vw;
+	}
+
+	.tui-content__box {
+		position: relative;
+		width: 100%;
+	}
+
+	.tui-content__title {
+		position: sticky;
+		top: 0;
+		z-index: 10;
+		font-weight: bold;
+	}
+
+	.tui-content__title .tui-title__item {
+		width: 100%;
+		position: relative;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-line__top::before {
+		content: ' ';
+		position: absolute;
+		top: 0;
+		right: 0;
+		left: 0;
+		border-top: 1px solid #ebedf0;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+	.tui-line__bottom::after {
+		content: ' ';
+		position: absolute;
+		border-bottom: 1px solid #ebedf0;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 100%;
+		bottom: 0;
+		right: 0;
+		left: 0;
+	}
+
+	.tui-index__indicator {
+		position: fixed;
+		right: 100rpx;
+		width: 100rpx;
+		height: 100rpx;
+		line-height: 100rpx;
+		border-radius: 10rpx;
+		text-align: center;
+		color: #ffffff;
+		font-size: 60rpx;
+		font-weight: bold;
+		display: none;
+		z-index: 10;
+	}
+
+	.tui-index__indicator:after {
+		content: '';
+		position: absolute;
+		top: 0;
+		right: 0;
+		width: 100%;
+		height: 100%;
+		z-index: -1;
+		border-radius: 100% 0% 100% 100%;
+		background: #c9c9c9;
+		transform: rotate(45deg);
+	}
+
+	.tui-indicator__show {
+		display: block;
+		z-index: 10;
+	}
+
+	.tui-indicator__tran {
+		display: block;
+		opacity: 0;
+		transition: opacity 0.3s linear;
+	}
+
+	.tui-index__letter {
+		position: fixed;
+		right: 0;
+		top: 50%;
+		transform: translateY(-50%);
+		text-align: center;
+		z-index: 10;
+	}
+
+	.tui-letter__item {
+		padding: 0 8rpx;
+		font-weight: bold;
+	}
+
+	.tui-letter__key {
+		width: 40rpx;
+		height: 40rpx;
+		border-radius: 50%;
+		font-size: 26rpx;
+		transform: scale(0.8);
+		transform-origin: center center;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-list__item {
+		width: 100%;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-list__item .tui-avatar {
+		width: 68rpx;
+		height: 68rpx;
+		border-radius: 8rpx;
+		flex-shrink: 0;
+		background-color: #ccc;
+	}
+
+	.tui-list__item view {
+		width: 90%;
+		font-size: 32rpx;
+		padding-left: 20rpx;
+		padding-right: 40rpx;
+		box-sizing: border-box;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+</style>

+ 578 - 0
components/thorui/tui-input/tui-input.vue

@@ -0,0 +1,578 @@
+<template>
+	<view class="tui-input__wrap"
+		:class="{'tui-border__top':borderTop && !inputBorder,'tui-border__bottom':borderBottom && !inputBorder,'tui-radius__fillet':isFillet && !getRadius,'tui-input__border-nvue':inputBorder}"
+		:style="getStyles" @tap="fieldClick">
+		<!-- #ifndef APP-NVUE -->
+		<view class="tui-input__border-top" v-if="borderTop && !inputBorder" :style="{borderTopColor:borderColor}">
+		</view>
+		<view class="tui-input__border-bottom" :class="{'tui-line__left':lineLeft}" v-if="borderBottom && !inputBorder"
+			:style="{borderBottomColor:borderColor}"></view>
+		<view class="tui-input__border" :class="{'tui-radius__fillet':isFillet && !getRadius}" v-if="inputBorder"
+			:style="{borderColor:borderColor,borderRadius:(getRadius*2)+'rpx'}"></view>
+		<!-- #endif -->
+		<!-- #ifdef APP-NVUE -->
+		<view class="tui-input__required" v-if="required">
+			<text :style="{color:getRequiredColor}">*</text>
+		</view>
+		<!-- #endif -->
+		<!-- #ifndef APP-NVUE -->
+		<view class="tui-input__required" :style="{color:getRequiredColor}" v-if="required">*</view>
+		<!-- #endif -->
+		<view class="tui-input__label"
+			:style="{fontSize:getLabelSize+'rpx',color:getLabelColor,minWidth:labelWidth+'rpx'}" v-if="label">
+			<text :style="{fontSize:getLabelSize+'rpx',color:getLabelColor}">{{label}}</text>
+		</view>
+		<slot name="left"></slot>
+		<input class="tui-input__self" :class="{'tui-text__right':textRight,'tui-input__disabled':disabled}"
+			:style="{fontSize:getSize+'rpx',color:color}" placeholder-class="tui-input__placeholder" :type="type"
+			:name="name" :value="inputVal" :password="password" :placeholder="inputVal?'':placeholder"
+			:placeholder-style="placeholderStyl" :disabled="disabled" :cursor-spacing="cursorSpacing"
+			:maxlength="maxlength" :focus="focused" :confirm-type="confirmType" :confirm-hold="confirmHold"
+			:cursor="cursor" :selection-start="selectionStart" :selection-end="selectionEnd"
+			:adjust-position="adjustPosition" :hold-keyboard="holdKeyboard" :auto-blur="autoBlur" @focus="onFocus"
+			@blur="onBlur" @input="onInput" @confirm="onConfirm" @keyboardheightchange="onKeyboardheightchange" />
+		<icon type="clear" :size="clearSize" :color="clearColor" v-if="clearable && inputVal != ''" @tap.stop="onClear">
+		</icon>
+		<slot name="right"></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-input",
+		emits: ['input', 'update:modelValue', 'focus', 'blur', 'confirm', 'click', 'keyboardheightchange'],
+		//这里加group是为了避免在表单中使用时给组件加value属性
+		// #ifdef MP-WEIXIN
+		behaviors: ['wx://form-field-group'],
+		// #endif
+		// #ifdef MP-BAIDU
+		behaviors: ['swan://form-field'],
+		// #endif
+		// #ifdef MP-QQ
+		behaviors: ['qq://form-field'],
+		// #endif
+		// #ifdef H5
+		behaviors: ['uni://form-field'],
+		// #endif
+		// #ifdef MP-WEIXIN
+		options: {
+			addGlobalClass: true,
+			virtualHost: true
+		},
+		// #endif
+		props: {
+			//是否为必填项
+			required: {
+				type: Boolean,
+				default: false
+			},
+			requiredColor: {
+				type: String,
+				default: ''
+			},
+			//左侧标题
+			label: {
+				type: String,
+				default: ''
+			},
+			//标题字体大小
+			labelSize: {
+				type: [Number, String],
+				default: 0
+			},
+			labelColor: {
+				type: String,
+				default: ''
+			},
+			//label 最小宽度 rpx
+			labelWidth: {
+				type: Number,
+				default: 140
+			},
+			clearable: {
+				type: Boolean,
+				default: false
+			},
+			//px
+			clearSize: {
+				type: Number,
+				default: 15
+			},
+			clearColor: {
+				type: String,
+				default: '#bfbfbf'
+			},
+			//获取焦点
+			focus: {
+				type: Boolean,
+				default: false
+			},
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			placeholderStyle: {
+				type: String,
+				default: ''
+			},
+			//输入框名称
+			name: {
+				type: String,
+				default: ''
+			},
+			//输入框值
+			value: {
+				type: [Number, String],
+				default: ''
+			},
+			// #ifdef VUE3
+			//输入框值
+			modelValue: {
+				type: [Number, String],
+				default: ''
+			},
+			// #endif
+			modelModifiers: {
+				default: () => ({})
+			},
+			//与官方input type属性一致
+			type: {
+				type: String,
+				default: 'text'
+			},
+			password: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			maxlength: {
+				type: [Number, String],
+				default: 140
+			},
+			min: {
+				type: [Number, String],
+				default: 'NaN'
+			},
+			max: {
+				type: [Number, String],
+				default: 'NaN'
+			},
+			cursorSpacing: {
+				type: Number,
+				default: 0,
+			},
+			confirmType: {
+				type: String,
+				default: 'done'
+			},
+			confirmHold: {
+				type: Boolean,
+				default: false,
+			},
+			cursor: {
+				type: Number,
+				default: -1
+			},
+			selectionStart: {
+				type: Number,
+				default: -1
+			},
+			selectionEnd: {
+				type: Number,
+				default: -1
+			},
+			adjustPosition: {
+				type: Boolean,
+				default: true
+			},
+			holdKeyboard: {
+				type: Boolean,
+				default: false
+			},
+			autoBlur: {
+				type: Boolean,
+				default: false
+			},
+			//输入框字体大小 rpx
+			size: {
+				type: [Number, String],
+				default: 0
+			},
+			//输入框字体颜色
+			color: {
+				type: String,
+				default: ''
+			},
+			// 是否显示 input 边框
+			inputBorder: {
+				type: Boolean,
+				default: false
+			},
+			borderColor: {
+				type: String,
+				default: 'rgba(0, 0, 0, 0.1)'
+			},
+			//input是否显示为圆角
+			isFillet: {
+				type: Boolean,
+				default: false
+			},
+			// 是否显示上边框
+			borderTop: {
+				type: Boolean,
+				default: false
+			},
+			// 是否显示下边框
+			borderBottom: {
+				type: Boolean,
+				default: true
+			},
+			//下边框线条是否有左偏移距离
+			lineLeft: {
+				type: Boolean,
+				default: true
+			},
+			// 是否自动去除两端的空格
+			trim: {
+				type: Boolean,
+				default: true
+			},
+			textRight: {
+				type: Boolean,
+				default: false
+			},
+			//输入框padding值
+			padding: {
+				type: String,
+				default: ''
+			},
+			//输入框背景颜色
+			backgroundColor: {
+				type: String,
+				default: ''
+			},
+			radius: {
+				type: [Number, String],
+				default: -1
+			},
+			//输入框margin-top值 rpx
+			marginTop: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		computed: {
+			getLabelSize() {
+				return this.labelSize || (uni && uni.$tui && uni.$tui.tuiInput.labelSize) || 32
+			},
+			getLabelColor() {
+				return this.labelColor || (uni && uni.$tui && uni.$tui.tuiInput.labelColor) || '#333'
+			},
+			getSize() {
+				return this.size || (uni && uni.$tui && uni.$tui.tuiInput.size) || 32
+			},
+			getColor() {
+				return this.color || (uni && uni.$tui && uni.$tui.tuiInput.color) || '#333'
+			},
+			getRadius() {
+				let radius = this.radius
+				if (radius === -1 || radius === true) {
+					radius = uni && uni.$tui && uni.$tui.tuiInput.radius
+				}
+				return Number(radius || 0)
+			},
+			getStyles() {
+				const padding = this.padding || (uni && uni.$tui && uni.$tui.tuiInput.padding) || '26rpx 30rpx';
+				const bgColor = this.backgroundColor || (uni && uni.$tui && uni.$tui.tuiInput.backgroundColor) ||
+					'#FFFFFF';
+				let radius = this.getRadius;
+				let styles = `padding:${padding};background:${bgColor};margin-top:${this.marginTop}rpx;`
+				if (radius && radius !== true && radius !== -1) {
+					styles += `border-radius:${radius}rpx;`
+				}
+				if (this.borderTop || this.borderBottom || this.inputBorder) {
+					styles += `border-color:${this.borderColor};`
+				}
+				return styles
+			},
+			getRequiredColor() {
+				return this.requiredColor || (uni && uni.$tui && uni.$tui.tuiInput.requiredColor) || '#EB0909'
+			}
+		},
+		data() {
+			return {
+				placeholderStyl: '',
+				focused: false,
+				inputVal: ''
+			}
+		},
+		watch: {
+			focus(val) {
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.focused = val
+					}, 50)
+				})
+			},
+			placeholderStyle() {
+				this.fieldPlaceholderStyle()
+			},
+			// #ifdef VUE3
+			modelValue(newVal) {
+				this.inputVal = newVal
+			},
+			// #endif
+			value(newVal) {
+				this.inputVal = newVal
+			}
+		},
+		created() {
+			this.fieldPlaceholderStyle()
+			setTimeout(() => {
+				// #ifndef VUE3
+				this.inputVal = this.value
+				// #endif
+
+				// #ifdef VUE3
+				if (this.value && !this.modelValue) {
+					this.inputVal = this.value
+				} else {
+					this.inputVal = this.modelValue
+				}
+				// #endif
+			}, 50)
+		},
+		mounted() {
+			this.$nextTick(() => {
+				setTimeout(() => {
+					this.focused = this.focus
+				}, 300)
+			})
+		},
+		methods: {
+			fieldPlaceholderStyle() {
+				if (this.placeholderStyle) {
+					this.placeholderStyl = this.placeholderStyle
+				} else {
+					const size = uni.upx2px(this.size || (uni && uni.$tui && uni.$tui.tuiInput.size) || 32)
+					this.placeholderStyl = `font-size:${size}px`
+				}
+			},
+			onInput(event) {
+				let value = event.detail.value;
+				if (this.trim) value = this.trimStr(value);
+				this.inputVal = value
+				//数字类型 数值不能超过最大整数安全范围,一但超过则返回字符串
+				const cVal = Number(value)
+				if ((this.modelModifiers.number || this.type === 'digit' || this.type === 'number') && !isNaN(cVal) &&
+					Number.isSafeInteger(cVal)) {
+					let eVal = this.type === 'digit' ? value : cVal
+					if (typeof cVal === 'number') {
+						const min = Number(this.min)
+						const max = Number(this.max)
+						if (typeof min === 'number' && cVal < min) {
+							eVal = min
+						} else if (typeof max === 'number' && max < cVal) {
+							eVal = max
+						}
+					}
+					value = isNaN(eVal) ? value : eVal
+				}
+				this.$nextTick(() => {
+					event.detail.value !== '' && (this.inputVal = value);
+				})
+				const inputValue = event.detail.value !== '' ? value : ''
+				this.$emit('input', inputValue);
+				// #ifdef VUE3
+				this.$emit('update:modelValue', inputValue)
+				// #endif
+			},
+			onFocus(event) {
+				this.$emit('focus', event);
+			},
+			onBlur(event) {
+				this.$emit('blur', event);
+			},
+			onConfirm(e) {
+				this.$emit('confirm', e);
+			},
+			onClear(event) {
+				if (this.disabled) return;
+				uni.hideKeyboard()
+				this.inputVal = '';
+				this.$emit('input', '');
+				// #ifdef VUE3
+				this.$emit('update:modelValue', '')
+				// #endif
+			},
+			fieldClick() {
+				this.$emit('click', {
+					name: this.name
+				});
+			},
+			onKeyboardheightchange(e) {
+				this.$emit('keyboardheightchange', e.detail)
+			},
+			trimStr(str) {
+				return str.replace(/^\s+|\s+$/g, '');
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-input__wrap {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		box-sizing: border-box;
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		position: relative;
+		flex: 1;
+		/* #ifdef APP-NVUE */
+		padding: 26rpx 30rpx;
+		/* #endif */
+		border-width: 0;
+	}
+
+	/* #ifndef APP-NVUE */
+
+	.tui-input__border-top {
+		position: absolute;
+		top: 0;
+		right: 0;
+		left: 0;
+		border-top: 1px solid var(--thorui-line-color, rgba(0, 0, 0, 0.1));
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+	.tui-input__border-bottom {
+		position: absolute;
+		border-bottom: 1px solid var(--thorui-line-color, rgba(0, 0, 0, 0.1));
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 100%;
+		bottom: 0;
+		right: 0;
+		left: 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+	.tui-line__left {
+		left: 30rpx !important;
+	}
+
+	/* #endif */
+
+	/* #ifdef APP-NVUE */
+	.tui-border__top {
+		border-top-width: 0.5px;
+		border-top-style: solid;
+	}
+
+	.tui-border__bottom {
+		border-top-width: 0.5px;
+		border-top-style: solid;
+	}
+
+	/* #endif */
+	.tui-input__required {
+		position: absolute;
+		left: 12rpx;
+		/* #ifndef APP-NVUE */
+		height: 30rpx;
+		top: 50%;
+		transform: translateY(-50%);
+		line-height: 1.15;
+		/* #endif */
+
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		align-items: center;
+		justify-content: center;
+		line-height: 1;
+		/* #endif */
+	}
+
+	.tui-input__label {
+		padding-right: 12rpx;
+		/* #ifndef APP-NVUE */
+		flex-shrink: 0;
+		/* #endif */
+	}
+
+	.tui-input__self {
+		flex: 1;
+		padding-right: 12rpx;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		overflow: visible;
+		/* #endif */
+		background-color: transparent;
+	}
+
+	.tui-input__placeholder {
+		/* #ifndef APP-NVUE */
+		color: var(--thorui-text-color-placeholder, #ccc);
+		overflow: visible;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		color: #ccc;
+		font-size: 32rpx;
+		/* #endif */
+	}
+
+	/* #ifdef MP */
+	::v-deep .tui-input__placeholder {
+		color: var(--thorui-text-color-placeholder, #ccc);
+		overflow: visible;
+	}
+
+	/* #endif */
+
+	/* #ifdef APP-NVUE */
+	.tui-input__border-nvue {
+		border-width: 0.5px;
+		border-style: solid;
+	}
+
+	/* #endif */
+
+	/* #ifndef APP-NVUE */
+	.tui-input__disabled {
+		pointer-events: none;
+	}
+
+	.tui-input__border {
+		position: absolute;
+		height: 200%;
+		width: 200%;
+		border: 1px solid var(--thorui-border-color, #d1d1d1);
+		transform-origin: 0 0;
+		transform: scale(0.5);
+		left: 0;
+		top: 0;
+		border-radius: 8rpx;
+		pointer-events: none;
+	}
+
+	/* #endif */
+
+	.tui-radius__fillet {
+		border-radius: 100px !important;
+	}
+
+	.tui-text__right {
+		text-align: right;
+	}
+</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>

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

@@ -0,0 +1,241 @@
+<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",
+		emits: ['click','close'],
+		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>

+ 54 - 0
components/thorui/tui-label/tui-label.vue

@@ -0,0 +1,54 @@
+<template>
+	<view class="tui-label__box" :class="{'tui-label__full':isFull}" :style="{padding:padding,margin:margin}"
+		@tap.stop="onClick">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	//该组件主要用于tui-radio,tui-checkbox,tui-switch组件外层,类似label功能
+	export default {
+		name: "tui-label",
+		props: {
+			padding: {
+				type: String,
+				default: '0'
+			},
+			margin: {
+				type: String,
+				default: '0'
+			},
+			isFull: {
+				type: Boolean,
+				default: false
+			}
+		},
+		created() {
+			this.childrens = [];
+		},
+		methods: {
+			onClick() {
+				if (this.childrens && this.childrens.length > 0) {
+					for (let child of this.childrens) {
+						child.labelClick()
+					}
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-label__box {
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.tui-label__full {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+	}
+</style>

+ 158 - 0
components/thorui/tui-landscape/tui-landscape.vue

@@ -0,0 +1,158 @@
+<template>
+	<view class="tui-landscape__box">
+		<view class="tui-landscape__inner" :style="{zIndex:zIndex}" v-if="show">
+			<slot></slot>
+			<view class="tui-icon__close"
+				:style="{top:position!=1?iconTop:'auto',bottom:position==1?iconBottom:'auto',left:position==3?iconLeft:(position==1?'50%':'auto'),right:position==2?iconRight:'auto'}"
+				:class="{'tui-icon__bottom':position==1}" v-if="closeIcon" @tap.stop="close">
+				<icon type="clear" :color="iconColor" :size="iconSize"></icon>
+			</view>
+		</view>
+		<view :style="{backgroundColor:maskBgColor,zIndex:maskZIndex}" @tap.stop="close(1)" class="tui-landscape__mask"
+			:class="{'tui-mask_hidden':!show}" v-if="mask"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-landscape",
+		emits: ['close'],
+		props: {
+			//是否显示
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//显示内容区z-index
+			zIndex: {
+				type: Number,
+				default: 1001
+			},
+			//是否需要关闭图标
+			closeIcon: {
+				type: Boolean,
+				default: true
+			},
+			//关闭图标颜色
+			iconColor: {
+				type: String,
+				default: '#fff'
+			},
+			//关闭图标大小 px
+			iconSize: {
+				type: Number,
+				default: 25
+			},
+			//icon位置:1-底部 2-右上角 3-左上角
+			position: {
+				type: [Number, String],
+				default: 1
+			},
+			//关闭图标top值,position为2或3的时候生效
+			iconTop: {
+				type: String,
+				default: '-120rpx'
+			},
+			//关闭图标bottom值,position为1的时候生效
+			iconBottom: {
+				type: String,
+				default: '-120rpx'
+			},
+			//关闭图标left值,position为3的时候生效
+			iconLeft: {
+				type: String,
+				default: '0'
+			},
+			//关闭图标right值,position为2的时候生效
+			iconRight: {
+				type: String,
+				default: '0'
+			},
+			//点击遮罩是否可以关闭
+			maskClosable: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要遮罩
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			//遮罩背景色
+			maskBgColor: {
+				type: String,
+				default: 'rgba(0,0,0,.6)'
+			},
+			//遮罩z-index值
+			maskZIndex: {
+				type: Number,
+				default: 1000
+			},
+			//自定义参数
+			params: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		methods: {
+			close(isMask) {
+				if (isMask == 1 && !this.maskClosable) return;
+				this.$emit('close', {
+					params: this.params
+				});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-landscape__box {
+		width: 100%;
+		box-sizing: border-box;
+		overflow: hidden;
+	}
+
+	.tui-landscape__inner {
+		max-width: 100%;
+		position: fixed;
+		box-sizing: border-box;
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+	}
+
+	.tui-icon__close {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		text-align: center;
+		position: absolute;
+		z-index: 10;
+	}
+
+	.tui-icon__bottom {
+		left: 50% !important;
+		transform: translateX(-50%);
+	}
+
+	.tui-landscape__mask {
+		position: fixed;
+		z-index: 1000;
+		top: 0;
+		right: 0;
+		left: 0;
+		bottom: 0;
+		opacity: 1;
+		transform: scale3d(1, 1, 1);
+		transition: all .2s ease-in
+	}
+
+	.tui-mask_hidden {
+		opacity: 0 !important;
+		transform: scale3d(1, 1, 0) !important;
+	}
+</style>

+ 223 - 0
components/thorui/tui-lazyload-img/tui-lazyload-img.vue

@@ -0,0 +1,223 @@
+<template>
+	<view class="tui-lazyload__box"
+		:style="{backgroundColor:placeholder?'transparent':backgroundColor,width:width,height:height?height:'auto',borderRadius:radius}"
+		@tap="handleClick">
+		<image class="tui-lazyload__img"
+			:class="{'tui-img__hidden':!placeholder && fadeShow && !show,'tui-img__appear':show && !placeholder && fadeShow}"
+			:style="{height:height,borderRadius:radius}" :src="show?src:placeholder"
+			:mode="height&&height!==true?mode:'widthFix'" :webp="webp" :show-menu-by-longpress="showMenuByLongpress"
+			:draggable="draggable" @load="load" @error="error" :id="elId">
+		</image>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-lazyload-img",
+		emits: ['error', 'load', 'click'],
+		options: {
+			virtualHost: true
+		},
+		props: {
+			//图片路径
+			src: {
+				type: String,
+				default: ''
+			},
+			//占位图路径
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			//占位背景色,placeholder有值时失效
+			backgroundColor: {
+				type: String,
+				default: '#E7E7E7'
+			},
+			//图片的裁剪模式,参考image组件mode属性
+			mode: {
+				type: String,
+				default: 'widthFix'
+			},
+			//图片显示动画效果,无占位图时有效
+			fadeShow: {
+				type: Boolean,
+				default: true
+			},
+			//默认不解析 webP 格式,只支持网络资源 微信小程序2.9.0
+			webp: {
+				type: Boolean,
+				default: false
+			},
+			//开启长按图片显示识别小程序码菜单 微信小程序2.7.0
+			showMenuByLongpress: {
+				type: Boolean,
+				default: false
+			},
+			//鼠标长按是否能拖动图片 仅H5平台 3.1.1+ 有效
+			draggable: {
+				type: Boolean,
+				default: true
+			},
+			//图片宽度
+			width: {
+				type: String,
+				default: '340rpx'
+			},
+			//图片高度,如果高度设置为auto,mode值需要设置为widthFix
+			height: {
+				type: String,
+				default: '340rpx'
+			},
+			//图片圆角值,如:10rpx
+			radius: {
+				type: String,
+				default: '0'
+			},
+			//节点布局区域的下边界,目标节点区域以下 bottom(px) 时,就会触发回调函数
+			bottom: {
+				type: [Number, String],
+				default: 50
+			},
+			//是否停止监听,设置为true时回调函数将不再触发
+			disconnect: {
+				type: Boolean,
+				default: false
+			},
+			//图片在列表中的索引值
+			index: {
+				type: Number,
+				default: 0
+			}
+		},
+		data() {
+			let elId = this.unique() + this.index
+			return {
+				show: false,
+				elId: elId
+			};
+		},
+		watch: {
+			disconnect(val) {
+				if (val) {
+					this.removeObserver()
+				}
+			}
+		},
+		created() {
+			this.observer = null;
+			// this.elId = this.unique() + this.index;
+		},
+		mounted() {
+			this.$nextTick(() => {
+				setTimeout(() => {
+					// #ifndef H5
+					if (!this.disconnect) {
+						this.initObserver()
+					} else {
+						this.show = true;
+					}
+					// #endif
+
+					// #ifdef H5
+					if (!this.disconnect && window.self === window.top) {
+						this.initObserver()
+					} else {
+						this.show = true;
+					}
+					// #endif
+				}, 50)
+			})
+		},
+		// #ifndef VUE3
+		beforeDestroy() {
+			this.removeObserver()
+		},
+		// #endif
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.removeObserver()
+		},
+		// #endif
+		methods: {
+			unique: function(n) {
+				n = n || 6;
+				let rnd = '';
+				for (let i = 0; i < n; i++)
+					rnd += Math.floor(Math.random() * 10);
+				return 'tui_' + new Date().getTime() + rnd;
+			},
+			removeObserver() {
+				if (this.observer) {
+					this.observer.disconnect()
+					this.observer = null;
+				}
+			},
+			initObserver() {
+				if (this.observer || this.show) return;
+				try {
+					let element = this.elId ? `#${this.elId}` : '.tui-lazyload__img';
+					const observer = uni.createIntersectionObserver(this)
+					observer.relativeToViewport({
+						bottom: Number(this.bottom) || 50
+					}).observe(element, (res) => {
+						if (res.intersectionRatio > 0 && !this.show) {
+							this.show = true;
+							this.removeObserver()
+						}
+					})
+					this.observer = observer
+				} catch (e) {
+					//TODO handle the exception
+					this.show = true;
+					this.removeObserver()
+				}
+			},
+			error(e) {
+				if (!this.show) return;
+				this.$emit('error', {
+					detail: e.detail,
+					index: this.index
+				})
+			},
+			load(e) {
+				if (!this.show) return;
+				this.$emit('load', {
+					detail: e.detail,
+					index: this.index
+				})
+			},
+			handleClick(e) {
+				this.$emit('click', {
+					index: this.index
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-lazyload__box {
+		display: inline-flex;
+		position: relative;
+		flex-shrink: 0;
+	}
+
+	.tui-lazyload__img {
+		width: 100%;
+		display: block;
+		flex-shrink: 0;
+		transition: opacity .3s linear;
+	}
+
+	.tui-img__hidden {
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-img__appear {
+		opacity: 1;
+		visibility: visible;
+	}
+</style>

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

@@ -0,0 +1,178 @@
+<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',
+	emits: ['click'],
+	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;
+}
+/* #ifdef MP-BAIDU */
+.tui-list-cell:active {
+	background-color: #f1f1f1 !important;
+}
+/* #endif */
+
+.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>

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

@@ -0,0 +1,446 @@
+<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',
+	emits: ['click','cancel'],
+	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;
+	box-sizing: border-box;
+}
+
+/* #ifndef MP-QQ */
+.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;
+}
+/* #endif */
+
+.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;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #5677fc;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-primary-outline::after {
+	border: 1px solid #5677fc;
+}
+
+/* #endif */
+.tui-danger {
+	background: #ed3f14;
+	color: #fff;
+}
+
+.tui-danger-hover {
+	background: #d53912;
+	color: #e5e5e5;
+}
+
+.tui-danger-outline {
+	color: #ed3f14;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #ed3f14;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-danger-outline::after {
+	border: 1px solid #ed3f14;
+}
+/* #endif */
+
+.tui-red {
+	background: #e41f19;
+	color: #fff;
+}
+
+.tui-red-hover {
+	background: #c51a15;
+	color: #e5e5e5;
+}
+
+.tui-red-outline {
+	color: #e41f19;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #e41f19;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-red-outline::after {
+	border: 1px solid #e41f19;
+}
+/* #endif */
+
+.tui-warning {
+	background: #ff7900;
+	color: #fff;
+}
+
+.tui-warning-hover {
+	background: #e56d00;
+	color: #e5e5e5;
+}
+
+.tui-warning-outline {
+	color: #ff7900;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #ff7900;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-warning-outline::after {
+	border: 1px solid #ff7900;
+}
+
+/* #endif */
+.tui-green {
+	background: #19be6b;
+	color: #fff;
+}
+
+.tui-green-hover {
+	background: #16ab60;
+	color: #e5e5e5;
+}
+
+.tui-green-outline {
+	color: #19be6b;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #19be6b;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-green-outline::after {
+	border: 1px solid #19be6b;
+}
+/* #endif */
+
+.tui-white {
+	background: #fff;
+	color: #333;
+}
+
+.tui-white-hover {
+	background: #f7f7f9;
+	color: #666;
+}
+
+.tui-white-outline {
+	color: #333;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #333;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-white-outline::after {
+	border: 1px solid #333;
+}
+/* #endif */
+
+.tui-gray {
+	background: #ededed;
+	color: #999;
+}
+
+.tui-gray-hover {
+	background: #d5d5d5;
+	color: #898989;
+}
+
+.tui-gray-outline {
+	color: #999;
+	background: transparent;
+	/* #ifdef MP-QQ */
+	border: 1rpx solid #999;
+	/* #endif */
+}
+
+/* #ifndef MP-QQ */
+.tui-gray-outline::after {
+	border: 1px solid #999;
+}
+/* #endif */
+
+.tui-outline-hover {
+	opacity: 0.6;
+}
+
+.tui-circle-btn {
+	border-radius: 40rpx !important;
+}
+
+.tui-circle-btn::after {
+	border-radius: 80rpx !important;
+}
+</style>

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

@@ -0,0 +1,249 @@
+<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></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiNavigationBar',
+		emits: ['init', 'change'],
+		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: 996
+			}
+		},
+		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>

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

@@ -0,0 +1,118 @@
+<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'}" mode="widthFix"></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",
+		emits: ['click'],
+		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>

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

@@ -0,0 +1,240 @@
+<template>
+	<view class="tui-numberbox">
+		<view class="tui-num__icon__box" :style="{background:iconBgColor,borderRadius:radius}" @tap="reduce"
+			:class="[disabled || min >= inputValue ? 'tui-disabled' : '']">
+			<text class="tui-numbox-icon tui-num__icon-reduce"
+				:style="{ color: iconColor, fontSize: iconSize + 'rpx',lineHeight:iconSize + 'rpx' }"></text>
+		</view>
+		<input type="number" v-model="inputValue" :disabled="disabled" @blur="blur" class="tui-num-input"
+			:style="{ color: color, fontSize: size + 'rpx', background: backgroundColor, height: height + 'rpx', minHeight: height + 'rpx', width: width + 'rpx' }" />
+		<view class="tui-num__icon__box" :style="{background:iconBgColor,borderRadius:radius}" @tap="plus"
+			:class="[disabled || inputValue >= max ? 'tui-disabled' : '']">
+			<text class="tui-numbox-icon tui-num__icon-plus"
+				:style="{ color: iconColor, fontSize: iconSize + 'rpx',lineHeight:iconSize + 'rpx' }"></text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiNumberbox',
+		emits: ['change'],
+		props: {
+			value: {
+				type: [Number, String],
+				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
+			},
+			iconBgColor: {
+				type: String,
+				default: 'transparent'
+			},
+			radius:{
+				type: String,
+				default: '50%'
+			},
+			//加减图标大小 rpx
+			iconSize: {
+				type: Number,
+				default: 22
+			},
+			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(val, step) {
+				let scale = 1;
+				let scaleVal = 1;
+				//浮点型
+				if (!Number.isInteger(step)) {
+					scale = Math.pow(10, (step + '').split('.')[1].length);
+				}
+				//浮点型
+				if (!Number.isInteger(val)) {
+					scaleVal = Math.pow(10, (val + '').split('.')[1].length);
+				}
+				return Math.max(scale, scaleVal);
+			},
+			calcNum: function(type) {
+				if (this.disabled || (this.inputValue == this.min && type === 'reduce') || (this.inputValue == this
+						.max && type === 'plus')) {
+					return;
+				}
+				const scale = this.getScale(Number(this.inputValue), Number(this.step));
+				let num = Number(this.inputValue) * scale;
+				let step = this.step * scale;
+				if (type === 'reduce') {
+					num -= step;
+				} else if (type === 'plus') {
+					num += step;
+				}
+				let value = Number((num / scale).toFixed(String(scale).length - 1));
+				if (value < this.min) {
+					value = this.min;
+				} else if (value > this.max) {
+					value = this.max;
+				}
+				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) && Number.isInteger(Number(value))) {
+						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: Number(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-num__icon__box {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		text-align: center;
+		padding: 12rpx;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-numbox-icon {
+		font-family: 'numberbox' !important;
+		font-style: normal;
+		-webkit-font-smoothing: antialiased;
+		-moz-osx-font-smoothing: grayscale;
+	}
+
+	.tui-num__icon-reduce:before {
+		content: '\e691';
+	}
+
+	.tui-num__icon-plus:before {
+		content: '\e605';
+	}
+
+	.tui-numberbox {
+		/* #ifndef APP-NVUE */
+		display: inline-flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.tui-num-input {
+		text-align: center;
+		margin: 0 6rpx;
+		font-weight: 400;
+		padding: 0;
+	}
+
+	.tui-disabled {
+		opacity: .5;
+		/* #ifdef H5 */
+		cursor: not-allowed;
+		/* #endif */
+	}
+</style>

+ 210 - 0
components/thorui/tui-pagination/tui-pagination.vue

@@ -0,0 +1,210 @@
+<template>
+	<view class="tui-pagination__box">
+		<view class="tui-pagination__btn"
+			:class="{'tui-pagination__disabled':currentIndex === 1,'tui-pagination__hover':currentIndex !== 1}"
+			:style="{width:width+'rpx',height:height+'rpx',borderColor:borderColor,backgroundColor:backgroundColor,borderRadius:radius}"
+			@click="clickPrev">
+			<text :style="{color:color,fontSize:size+'rpx'}" v-if="!custom">{{prevText}}</text>
+			<slot name="prev"></slot>
+		</view>
+		<view class="tui-pagination__num" v-if="isPage">
+			<text :style="{color:currentColor,fontSize:pageFontSize+'rpx'}">{{currentIndex}}</text>
+			<text :style="{color:pageColor,fontSize:pageFontSize+'rpx'}">/{{maxPage || 0}}</text>
+		</view>
+		<view class="tui-pagination__btn"
+			:class="{'tui-pagination__disabled':currentIndex === maxPage,'tui-pagination__hover':currentIndex !== maxPage}"
+			:style="{width:width+'rpx',height:height+'rpx',borderColor:borderColor,backgroundColor:backgroundColor,borderRadius:radius}"
+			@click="clickNext">
+			<text :style="{color:color,fontSize:size+'rpx'}" v-if="!custom">{{nextText}}</text>
+			<slot name="next"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-pagination",
+		emits: ['prev', 'next', 'change'],
+		props: {
+			prevText: {
+				type: String,
+				default: '上一页'
+			},
+			nextText: {
+				type: String,
+				default: '下一页'
+			},
+			width: {
+				type: Number,
+				default: 156
+			},
+			height: {
+				type: Number,
+				default: 68
+			},
+			borderColor: {
+				type: String,
+				default: 'transparent'
+			},
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			color: {
+				type: String,
+				default: '#333'
+			},
+			size: {
+				type: [Number, String],
+				default: 28
+			},
+			radius: {
+				type: String,
+				default: '8rpx'
+			},
+			//是否自定义按钮显示内容
+			custom: {
+				type: Boolean,
+				default: false
+			},
+			//当前页码
+			current: {
+				type: [Number, String],
+				default: 1
+			},
+			//当前页码字体颜色
+			currentColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//页码字体颜色
+			pageColor: {
+				type: String,
+				default: '#333'
+			},
+			//页码字体大小
+			pageFontSize: {
+				type: [Number, String],
+				default: 36
+			},
+			//是否需要展示页码
+			isPage: {
+				type: Boolean,
+				default: true
+			},
+			//数据总量
+			total: {
+				type: [Number, String],
+				default: 0
+			},
+			//每页数据量
+			pageSize: {
+				type: [Number, String],
+				default: 10
+			}
+		},
+		computed: {
+			maxPage() {
+				let maxPage = 1
+				let total = Number(this.total)
+				let pageSize = Number(this.pageSize)
+				if (total && pageSize) {
+					maxPage = Math.ceil(total / pageSize)
+				}
+				return maxPage
+			}
+		},
+		watch: {
+			current(val) {
+				this.currentIndex = +val
+			}
+		},
+		created() {
+			this.currentIndex = +this.current
+		},
+		data() {
+			return {
+				currentIndex: 1
+			};
+		},
+		methods: {
+			clickPrev() {
+				if (Number(this.currentIndex) === 1) return;
+				this.currentIndex -= 1
+				this.change('prev')
+			},
+			clickNext() {
+				if (Number(this.currentIndex) === this.maxPage) return;
+				this.currentIndex += 1
+				this.change('next')
+			},
+			change(e) {
+				this.$emit('change', {
+					type: e,
+					current: this.currentIndex
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-pagination__box {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		display: flex;
+		/* #endif */
+		position: relative;
+		overflow: hidden;
+		flex-direction: row;
+		justify-content: space-between;
+		align-items: center;
+	}
+
+	.tui-pagination__btn {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		position: relative;
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		text-align: center;
+		/* #ifdef APP-NVUE */
+		border-width: 0.5px;
+		/* #endif */
+
+		/* #ifndef APP-NVUE */
+		border-width: 1rpx;
+		/* #endif */
+		border-style: solid;
+		flex-shrink: 0;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-pagination__hover:active {
+		opacity: 0.5;
+	}
+
+
+	.tui-pagination__num {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex: 1;
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.tui-pagination__disabled {
+		opacity: 0.3;
+		/* #ifdef H5 */
+		cursor: not-allowed;
+		/* #endif */
+	}
+</style>

+ 502 - 0
components/thorui/tui-picker/tui-picker.vue

@@ -0,0 +1,502 @@
+<template>
+	<view class="tui-picker__box">
+		<view class="tui-mask__screen" :class="[visible?'tui-picker__mask-show':'']" @tap="maskClick"></view>
+		<view class="tui-picker__wrap" :style="{backgroundColor:backgroundColor}"
+			:class="[visible?'tui-picker__show':'',radius?'tui-picker__radius':'']">
+			<view class="tui-picker__header" :style="{backgroundColor:headerBgColor}">
+				<view class="tui-picker__btn-cancle" hover-class="tui-picker__opcity" :hover-stay-time="150"
+					@tap.stop="hidePicker"
+					:style="{color:cancelColor,fontSize:btnSize+'rpx',fontWeight:bold?'bold':'normal'}">{{cancelText}}
+				</view>
+				<view class="tui-picker__title" :style="{fontSize:titleSize+'rpx',color:titleColor}">{{title}}</view>
+				<view class="tui-picker__btn-sure" hover-class="tui-picker__opcity" :hover-stay-time="150"
+					@tap.stop="picker"
+					:style="{color:confirmColor,fontSize:btnSize+'rpx',fontWeight:bold?'bold':'normal'}">{{confirmText}}
+				</view>
+			</view>
+			<picker-view :mask-style="maskStyle" :indicator-style="indicatorStyle" class="tui-picker__view"
+				:value="vals" @change="columnPicker" @pickstart="pickstart" @pickend="pickend">
+				<picker-view-column>
+					<view :style="{color:color,fontSize:size+'px'}" v-for="(item,index) in layer1__data" :key="index"
+						class="tui-picker__item">{{item}}</view>
+				</picker-view-column>
+				<picker-view-column v-if="layer==2 || layer==3">
+					<view :style="{color:color,fontSize:size+'px'}" v-for="(item,index) in layer2__data" :key="index"
+						class="tui-picker__item">{{item}}</view>
+				</picker-view-column>
+				<picker-view-column v-if="layer==3">
+					<view :style="{color:color,fontSize:size+'px'}" v-for="(item,index) in layer3__data" :key="index"
+						class="tui-picker__item">{{item}}</view>
+				</picker-view-column>
+			</picker-view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-picker",
+		emits: ['pickstart', 'pickend', 'hide', 'change'],
+		props: {
+			//数据层级
+			layer: {
+				type: [Number, String],
+				default: 1
+			},
+			//data数据
+			pickerData: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//是否显示
+			show: {
+				type: Boolean,
+				default: false
+			},
+			//默认值,text集合
+			value: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//设置选择器中间选中框的样式
+			indicatorStyle: {
+				type: String,
+				default: 'height: 48px;'
+			},
+			//设置蒙层的样式
+			maskStyle: {
+				type: String,
+				default: ''
+			},
+			//是否显示圆角
+			radius: {
+				type: Boolean,
+				default: false
+			},
+			//header背景色
+			headerBgColor: {
+				type: String,
+				default: '#fff'
+			},
+			//显示标题
+			title: {
+				type: String,
+				default: ''
+			},
+			//标题字体大小
+			titleSize: {
+				type: [Number, String],
+				default: 34
+			},
+			//标题字体颜色
+			titleColor: {
+				type: String,
+				default: '#333'
+			},
+			//确认按钮文本
+			confirmText: {
+				type: String,
+				default: '确定'
+			},
+			//确认按钮文本颜色
+			confirmColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//取消按钮文本
+			cancelText: {
+				type: String,
+				default: '取消'
+			},
+			//取消按钮文本颜色
+			cancelColor: {
+				type: String,
+				default: '#888'
+			},
+			//按钮字体大小
+			btnSize: {
+				type: [Number, String],
+				default: 32
+			},
+			//按钮字体是否加粗
+			bold: {
+				type: Boolean,
+				default: true
+			},
+			//内容背景色
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			//内容字体颜色
+			color: {
+				type: String,
+				default: '#333'
+			},
+			//picker内容字体大小 px
+			size: {
+				type: [Number, String],
+				default: 16
+			},
+			//点击遮罩 是否可关闭
+			maskClosable: {
+				type: Boolean,
+				default: true
+			},
+			//自定义参数
+			params: {
+				type: [Number, String],
+				default: 0
+			}
+		},
+		data() {
+			return {
+				visible: false,
+				vals: [0],
+				layer1__data: [],
+				layer2__data: [],
+				layer3__data: [],
+				isEnd: true,
+				timer: null
+			};
+		},
+		created() {
+			this.initData(-1, 0, 0);
+			setTimeout(() => {
+				this.setDefaultOptions()
+			}, 50)
+			this.visible = this.show;
+		},
+		watch: {
+			show(val) {
+				this.visible = val;
+			},
+			value(vals) {
+				if (vals && vals.length > 0) {
+					this.setDefaultOptions()
+				}
+			},
+			pickerData(newVal) {
+				this.initData(-1, 0, 0)
+				setTimeout(() => {
+					this.setDefaultOptions()
+				}, 50)
+			}
+		},
+		methods: {
+			hidePicker() {
+				this.visible = false;
+				this.$emit('hide', {
+					params: this.params
+				})
+			},
+			maskClick() {
+				if (!this.maskClosable) return;
+				this.hidePicker()
+			},
+			getValue(key = 'text', layer = 1) {
+				let vals = this.vals;
+				let data = this.pickerData;
+				let result = ''
+				if (layer === 1) {
+					result = data[vals[0]][key]
+				} else if (layer == 2) {
+					if (data[vals[0]].children) {
+						result = data[vals[0]].children[vals[1]][key]
+					}
+				} else {
+					if (data[vals[0]].children && data[vals[0]].children[vals[1]].children) {
+						result = data[vals[0]].children[vals[1]].children[vals[2]][key]
+					}
+				}
+				return result;
+			},
+			loop() {
+				if (this.isEnd) {
+					this.pickerChange()
+				} else {
+					setTimeout(() => {
+						this.loop()
+					}, 50)
+				}
+			},
+			picker() {
+				this.hidePicker()
+				// #ifdef MP-WEIXIN
+				this.loop()
+				// #endif
+				// #ifndef MP-WEIXIN
+				this.pickerChange()
+				// #endif
+			},
+			pickerChange() {
+				if(!this.show) return;
+				let text = [];
+				let value = [];
+				let result = '';
+				if (this.pickerData.length > 0) {
+					if (this.layer == 1) {
+						text = this.getValue();
+						value = this.getValue('value');
+						result = text;
+					} else if (this.layer == 2) {
+						text = [this.getValue(), this.getValue('text', 2)];
+						value = [this.getValue('value'), this.getValue('value', 2)];
+						result = text.join('');
+					} else {
+						text = [this.getValue(), this.getValue('text', 2), this.getValue('text', 3)];
+						value = [this.getValue('value'), this.getValue('value', 2), this.getValue('value', 3)];
+						result = text.join('');
+					}
+				}
+
+				this.$emit('change', {
+					text: text,
+					value: value,
+					index: this.vals,
+					result: result,
+					params: this.params
+				})
+			},
+			toArr(data) {
+				let arr = [];
+				if (data && data.length > 0) {
+					for (let item of data) {
+						arr.push(item.text);
+					}
+				}
+				return arr;
+			},
+			checkChildrenData(data, layer, index, idx) {
+				let arr = [];
+				if (layer === 1) {
+					if (data[index])
+						arr = data[index].children || [];
+				} else {
+					if (data[index] && data[index].children && data[index].children[idx])
+						arr = data[index].children[idx].children || [];
+				}
+				return arr;
+			},
+			setDefaultOptions() {
+				let textArr = this.value;
+				let vals = [];
+				if (this.layer1__data.length > 0 && textArr.length > 0) {
+					textArr.forEach((item, idx) => {
+						let index = this[`layer${idx+1}__data`].indexOf(item);
+						if (idx === 0) {
+							this.layer2__data = this.toArr(this.checkChildrenData(this.pickerData, 1, index))
+						} else if (idx === 1) {
+							this.layer3__data = this.toArr(this.checkChildrenData(this.pickerData, 2, vals[0],
+								index))
+						}
+						if (index === -1) {
+							vals.push(0)
+						} else {
+							vals.push(index)
+						}
+					})
+				} else {
+					if (this.layer == 1) {
+						vals = [0]
+					} else if (this.layer == 2) {
+						vals = [0, 0];
+						this.layer2__data = this.toArr(this.checkChildrenData(this.pickerData, 1, 0))
+					} else {
+						vals = [0, 0, 0];
+						this.layer2__data = this.toArr(this.checkChildrenData(this.pickerData, 1, 0))
+						this.layer3__data = this.toArr(this.checkChildrenData(this.pickerData, 2, 0,
+							0))
+					}
+				}
+				this.vals = vals;
+			},
+			initData(layer, index, idx) {
+				let data = this.pickerData;
+				if (!data || data.length === 0) return;
+				if (this.layer == 1) {
+					this.layer1__data = this.toArr(data)
+				} else if (this.layer == 2) {
+					if (layer === -1)
+						this.layer1__data = this.toArr(data)
+
+					this.layer2__data = this.toArr(this.checkChildrenData(data, 1, index))
+				} else {
+					if (layer === -1)
+						this.layer1__data = this.toArr(data)
+
+					if (layer === 0 || layer === -1)
+						this.layer2__data = this.toArr(this.checkChildrenData(data, 1, index))
+
+					this.layer3__data = this.toArr(this.checkChildrenData(data, 2, index, idx))
+				}
+			},
+			columnPicker: function(e) {
+				let value = e.detail.value;
+				if (this.layer == 1) {
+					this.layer__one(value)
+				} else if (this.layer == 2) {
+					this.layer__two(value)
+				} else {
+					this.layer__three(value)
+				}
+			},
+			layer__one(value) {
+				if (this.vals[0] !== value[0]) {
+					this.vals = value;
+				}
+			},
+			layer__two(value) {
+				if (this.vals[0] !== value[0]) {
+					this.initData(0, value[0])
+					this.vals = [value[0], 0]
+				} else {
+					this.vals = value
+				}
+			},
+			layer__three(value) {
+				if (this.vals[0] !== value[0]) {
+					this.initData(0, value[0], 0)
+					this.vals = [value[0], 0, 0]
+				} else if (this.vals[1] !== value[1]) {
+					this.initData(0, value[0], value[1])
+					this.vals = [value[0], value[1], 0]
+				} else {
+					this.vals = value
+				}
+			},
+			pickstart(e) {
+				// #ifdef MP-WEIXIN
+				clearTimeout(this.timer)
+				this.isEnd = false;
+				// #endif
+				//仅微信小程序支持
+				this.$emit('pickstart', {
+					params: this.params
+				})
+			},
+			pickend(e) {
+				//仅微信小程序支持
+				// #ifdef MP-WEIXIN
+				this.timer = setTimeout(() => {
+					this.isEnd = true;
+				}, 100)
+				// #endif
+				this.$emit('pickend', {
+					params: this.params
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-mask__screen {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.6);
+		z-index: 1001;
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-picker__mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+
+	.tui-picker__wrap {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 1002;
+		visibility: hidden;
+		transform: translate3d(0, 100%, 0);
+		transform-origin: center;
+		transition: all 0.3s ease-in-out;
+		min-height: 20rpx;
+	}
+
+	.tui-picker__radius {
+		border-top-left-radius: 24rpx;
+		border-top-right-radius: 24rpx;
+		overflow: hidden;
+	}
+
+	.tui-picker__show {
+		transform: translate3d(0, 0, 0);
+		visibility: visible;
+	}
+
+	.tui-picker__header {
+		width: 100%;
+		height: 92rpx;
+		padding: 0 30rpx;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		box-sizing: border-box;
+		position: relative;
+	}
+
+	.tui-picker__header::after {
+		content: '';
+		position: absolute;
+		border-bottom: 1rpx solid rgba(0, 0, 0, .1);
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		bottom: 0;
+		right: 0;
+		left: 0;
+	}
+
+	.tui-picker__btn-cancle {
+		padding: 20rpx;
+		flex-shrink: 0;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-picker__btn-sure {
+		padding: 20rpx;
+		flex-shrink: 0;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-picker__title {
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		flex: 1;
+		padding: 0 30rpx;
+		box-sizing: border-box;
+		text-align: center;
+	}
+
+	.tui-picker__view {
+		width: 100%;
+		height: 260px;
+	}
+
+	.tui-picker__item {
+		line-height: 48px;
+		text-align: center;
+	}
+
+	.tui-picker__opcity {
+		opacity: 0.5;
+	}
+</style>

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

@@ -0,0 +1,675 @@
+<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" :class="{'tui-cropper__image-hidden':!imageUrl}" :src="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',
+		emits: ['ready', 'cropper', 'initAngle', 'imageLoad'],
+		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.apFilePath;
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								}
+							});
+						}
+						// #endif
+
+						// #ifndef MP-ALIPAY
+						let isBase64 = this.isBase64
+						// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+						isBase64 = false;
+						// #endif
+						if (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.windowHeight ? 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;
+		padding: 0;
+		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;
+		padding: 0;
+		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__image-hidden {
+		visibility: hidden;
+		opacity: 0;
+	}
+
+	.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>

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

@@ -0,0 +1,571 @@
+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;
+	var tcb = ins.selectComponent('.tui-cropper-box');
+	var tcm = ins.selectComponent('.tui-content-middle')
+	var tct = ins.selectComponent('.tui-content-top')
+	var twb = ins.selectComponent('.tui-wxs-bg')
+	if (!tcb || !tcm || !tct || !twb) return;
+	tcb.setStyle({
+		'width': cropper.canvasWidth + 'px',
+		'height': cropper.canvasHeight + 'px'
+	})
+	tcm.setStyle({
+		'height': cropper.canvasHeight + 'px'
+	})
+	tct.setStyle({
+		'height': cropper.cutY + 'px'
+	})
+	twb.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
+}

+ 269 - 0
components/thorui/tui-popup/tui-popup.vue

@@ -0,0 +1,269 @@
+<template>
+	<view class="tui-popup__transition" :class="[ani.in]" :style="'transform:' + transform + ';' + stylesObject" @click="change" v-if="isShow" ref="ani"><slot></slot></view>
+</template>
+
+<script>
+// #ifdef APP-NVUE
+const animation = uni.requireNativePlugin('animation');
+// #endif
+
+export default {
+	name: 'tuiPopup',
+	emits: ['click','change'],
+	props: {
+		show: {
+			type: Boolean,
+			default: false
+		},
+		/*
+		  [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out]
+		  过渡动画类型
+		*/
+		
+		modeClass: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		duration: {
+			type: Number,
+			default: 300
+		},
+		//styles 组件样式,同 css 样式
+		styles: {
+			type: Object,
+			default() {
+				return {
+					position: 'fixed',
+					bottom: 0,
+					top: 0,
+					left: 0,
+					right: 0,
+					/* #ifndef APP-NVUE */
+					display: 'flex',
+					/* #endif */
+					'justify-content': 'center',
+					'align-items': 'center'
+				};
+			}
+		}
+	},
+	data() {
+		return {
+			isShow: false,
+			transform: '',
+			ani: {
+				in: '',
+				active: ''
+			}
+		};
+	},
+	watch: {
+		show: {
+			handler(newVal) {
+				if (newVal) {
+					this.open();
+				} else {
+					this.close();
+				}
+			},
+			immediate: true
+		}
+	},
+	computed: {
+		stylesObject() {
+			let styles = {
+				...this.styles,
+				'transition-duration': this.duration / 1000 + 's'
+			};
+			let transfrom = '';
+			for (let i in styles) {
+				let line = this.toLine(i);
+				transfrom += line + ':' + styles[i] + ';';
+			}
+			return transfrom;
+		}
+	},
+	methods: {
+		change() {
+			this.$emit('click', {
+				detail: this.isShow
+			});
+		},
+		open() {
+			clearTimeout(this.timer);
+			this.isShow = true;
+			this.transform = '';
+			this.ani.in = '';
+			for (let i in this.getTranfrom(false)) {
+				if (i === 'opacity') {
+					this.ani.in = 'fade-in';
+				} else {
+					this.transform += `${this.getTranfrom(false)[i]} `;
+				}
+			}
+			this.$nextTick(() => {
+				setTimeout(() => {
+					this._animation(true);
+				}, 50);
+			});
+		},
+		close(type) {
+			clearTimeout(this.timer);
+			this._animation(false);
+		},
+		_animation(type) {
+			let styles = this.getTranfrom(type);
+			// #ifdef APP-NVUE
+			if (!this.$refs['ani']) return;
+			animation.transition(
+				this.$refs['ani'].ref,
+				{
+					styles,
+					duration: this.duration, //ms
+					timingFunction: 'ease',
+					needLayout: false,
+					delay: 0 //ms
+				},
+				() => {
+					if (!type) {
+						this.isShow = false;
+					}
+					this.$emit('change', {
+						detail: this.isShow
+					});
+				}
+			);
+			// #endif
+			// #ifndef APP-NVUE
+			this.transform = '';
+			for (let i in styles) {
+				if (i === 'opacity') {
+					this.ani.in = `fade-${type ? 'out' : 'in'}`;
+				} else {
+					this.transform += `${styles[i]} `;
+				}
+			}
+			this.timer = setTimeout(() => {
+				if (!type) {
+					this.isShow = false;
+				}
+				this.$emit('change', {
+					detail: this.isShow
+				});
+			}, this.duration);
+			// #endif
+		},
+		getTranfrom(type) {
+			let styles = {
+				transform: ''
+			};
+			this.modeClass.forEach(mode => {
+				switch (mode) {
+					case 'fade':
+						styles.opacity = type ? 1 : 0;
+						break;
+					case 'slide-top':
+						styles.transform += `translateY(${type ? '0' : '-100%'}) `;
+						break;
+					case 'slide-right':
+						styles.transform += `translateX(${type ? '0' : '100%'}) `;
+						break;
+					case 'slide-bottom':
+						styles.transform += `translateY(${type ? '0' : '100%'}) `;
+						break;
+					case 'slide-left':
+						styles.transform += `translateX(${type ? '0' : '-100%'}) `;
+						break;
+					case 'zoom-in':
+						styles.transform += `scale(${type ? 1 : 0.8}) `;
+						break;
+					case 'zoom-out':
+						styles.transform += `scale(${type ? 1 : 1.2}) `;
+						break;
+				}
+			});
+			return styles;
+		},
+		_modeClassArr(type) {
+			let mode = this.modeClass;
+			if (typeof mode !== 'string') {
+				let modestr = '';
+				mode.forEach(item => {
+					modestr += item + '-' + type + ',';
+				});
+				return modestr.substr(0, modestr.length - 1);
+			} else {
+				return mode + '-' + type;
+			}
+		},
+		toLine(name) {
+			return name.replace(/([A-Z])/g, '-$1').toLowerCase();
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-popup__transition {
+	transition-timing-function: ease;
+	transition-duration: 0.3s;
+	transition-property: transform, opacity;
+	position: relative;
+	z-index: 99;
+}
+
+.fade-in {
+	opacity: 0;
+}
+
+.fade-out {
+	opacity: 1;
+}
+
+.slide-top-in {
+	transform: translateY(-100%);
+}
+
+.slide-top-active {
+	transform: translateY(0);
+}
+
+.slide-right-in {
+	transform: translateX(100%);
+}
+
+.slide-right-active {
+	transform: translateX(0);
+}
+
+.slide-bottom-in {
+	transform: translateY(100%);
+}
+
+.slide-bottom-active {
+	transform: translateY(0);
+}
+
+.slide-left-in {
+	transform: translateX(-100%);
+}
+
+.slide-left-active {
+	transform: translateX(0);
+	opacity: 1;
+}
+
+.zoom-in-in {
+	transform: scale(0.8);
+}
+
+.zoom-out-active {
+	transform: scale(1);
+}
+
+.zoom-out-in {
+	transform: scale(1.2);
+}
+</style>

+ 652 - 0
components/thorui/tui-poster/tui-poster.vue

@@ -0,0 +1,652 @@
+<template>
+	<canvas :style="{ width: cv_width + 'px', height: cv_height + 'px' }" :canvas-id="posterId" :id="posterId"
+		class="tui-poster__cv"></canvas>
+</template>
+
+<script>
+	// #ifdef MP-WEIXIN
+	const posterId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
+	// #endif
+	export default {
+		name: "tui-poster",
+		emits: ['ready'],
+		props: {
+			//海报宽度,单位rpx
+			width: {
+				type: [Number, String],
+				default: 800
+			},
+			//海报高度,单位rpx
+			height: {
+				type: [Number, String],
+				default: 1200
+			},
+			//像素比
+			pixel: {
+				type: [Number, String],
+				default: 3
+			}
+		},
+		watch: {
+			width(val) {
+				this.cv_width = this.getPX(this.width)
+			},
+			height(val) {
+				this.cv_height = this.getPX(this.height)
+			}
+		},
+		data() {
+			// #ifndef MP-WEIXIN
+			const posterId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			// #endif
+			return {
+				posterId: posterId,
+				cv_width: 400,
+				cv_height: 600
+			};
+		},
+		created() {
+			this.cv_width = this.getPX(this.width)
+			this.cv_height = this.getPX(this.height)
+			this.ctx = null
+		},
+		mounted() {
+			setTimeout(() => {
+				this.ctx = uni.createCanvasContext(this.posterId, this)
+				this.$emit('ready', {})
+			}, 50)
+		},
+		methods: {
+			toast(msg) {
+				uni.showToast({
+					title: msg,
+					icon: 'none'
+				})
+			},
+			getPX(val) {
+				return uni.upx2px(Number(val) * Number(this.pixel))
+			},
+			getTextWidth(context, text, fontSize) {
+				let width = 0;
+				// #ifndef MP-ALIPAY || MP-BAIDU
+				width = context.measureText(text).width
+				// #endif
+				// #ifdef MP-ALIPAY || MP-BAIDU
+				let sum = 0;
+				for (let i = 0, len = text.length; i < len; i++) {
+					if (text.charCodeAt(i) >= 0 && text.charCodeAt(i) <= 255)
+						sum = sum + 1;
+					else
+						sum = sum + 2;
+				}
+				width = sum / 2 * this.getPX(fontSize)
+				// #endif
+				return width
+			},
+			getWrapText(text, fontSize, textWidth, width, ctx, rows = 2) {
+				let textArr = [];
+				if (textWidth > width) {
+					let fillText = '';
+					let lines = 1;
+					let arr = text.split('')
+					for (let i = 0, len = arr.length; i < len; i++) {
+						fillText = fillText + arr[i];
+						if (this.getTextWidth(ctx, fillText, fontSize) >= width) {
+							if (lines === rows && rows !== -1) {
+								if (i !== arr.length - 1) {
+									fillText = fillText.substring(0, fillText.length - 1) + '...';
+								}
+								textArr.push(fillText);
+								break;
+							}
+							textArr.push(fillText);
+							fillText = '';
+							lines++;
+						} else if (i === arr.length - 1) {
+							textArr.push(fillText);
+						}
+					}
+				} else {
+					textArr.push(text)
+				}
+				return textArr;
+			},
+			startDrawText(ctx, param) {
+				let styles = param.style || {}
+				let {
+					left,
+					top,
+					fontSize,
+					color,
+					baseLine = 'normal',
+					textAlign = 'left',
+					frontSize,
+					spacing,
+					opacity = 1,
+					lineThrough = false,
+					width = 600,
+					rows = 1,
+					lineHeight = 0,
+					fontWeight = 'normal',
+					fontStyle = 'normal',
+					fontFamily = "sans-serif"
+				} = styles;
+				ctx.save();
+				ctx.beginPath();
+				ctx.font = fontStyle + " " + fontWeight + " " + this.getPX(fontSize) + "px " + fontFamily
+				ctx.setGlobalAlpha(opacity);
+				// ctx.setFontSize(this.getPX(fontSize));
+				ctx.setFillStyle(color);
+				ctx.setTextBaseline(baseLine);
+				ctx.setTextAlign(textAlign);
+				let textWidth = this.getTextWidth(ctx, param.text, fontSize);
+				width = this.getPX(width);
+				let textArr = this.getWrapText(param.text, fontSize, textWidth, width, ctx, rows)
+				if (param.frontText) {
+					ctx.setFontSize(this.getPX(frontSize));
+					left = this.getTextWidth(ctx, param.frontText, frontSize) + this.getPX(left + spacing);
+					ctx.setFontSize(this.getPX(fontSize));
+				} else {
+					left = this.getPX(left)
+				}
+				textArr.forEach((item, index) => {
+					ctx.fillText(item, left, this.getPX(top + (lineHeight || fontSize) * index))
+				})
+				ctx.restore();
+				if (lineThrough) {
+					let lineY = top;
+					switch (baseLine) {
+						case 'top':
+							lineY += fontSize / 2 + 4;
+							break;
+						case 'middle':
+							break;
+						case 'bottom':
+							lineY -= fontSize / 2 + 4;
+							break;
+						default:
+							// #ifdef MP-WEIXIN
+							lineY -= fontSize / 2 - 3;
+							// #endif
+							// #ifndef MP-WEIXIN
+							lineY -= fontSize / 2 - 4;
+							// #endif
+							break;
+					}
+					ctx.save();
+					ctx.moveTo(left, this.getPX(lineY));
+					ctx.lineTo(left + textWidth + 2, this.getPX(lineY));
+					ctx.setStrokeStyle(color);
+					ctx.stroke();
+					ctx.restore();
+				}
+			},
+			drawRadiusRect(ctx, styles) {
+				let {
+					left,
+					top,
+					width,
+					height,
+					borderRadius
+				} = styles;
+				let r = this.getPX(borderRadius / 2);
+
+				left = this.getPX(left)
+				top = this.getPX(top)
+				width = this.getPX(width)
+				height = this.getPX(height)
+
+				ctx.beginPath();
+				ctx.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
+				ctx.moveTo(left + r, top);
+				ctx.lineTo(left + width - r, top);
+				ctx.lineTo(left + width, top + r);
+
+				ctx.arc(left + width - r, top + r, r, Math.PI * 1.5, Math.PI * 2);
+				ctx.lineTo(left + width, top + height - r);
+				ctx.lineTo(left + width - r, top + height);
+
+				ctx.arc(left + width - r, top + height - r, r, 0, Math.PI * 0.5);
+				ctx.lineTo(left + r, top + height);
+				ctx.lineTo(left, top + height - r);
+
+				ctx.arc(left + r, top + height - r, r, Math.PI * 0.5, Math.PI);
+				ctx.lineTo(left, top + r);
+				ctx.lineTo(left + r, top);
+			},
+			startDrawImage(ctx, param) {
+				let styles = param.style || {}
+				let {
+					left,
+					top,
+					width,
+					height,
+					borderRadius = 0,
+					borderWidth = 0,
+					borderColor
+				} = styles;
+				ctx.save();
+				if (borderRadius > 0) {
+					this.drawRadiusRect(ctx, styles);
+					ctx.strokeStyle = 'rgba(0,0,0,0)'
+					// #ifndef MP-BAIDU || MP-TOUTIAO
+				    ctx.stroke();
+					// #endif
+					ctx.clip();
+				}
+				ctx.drawImage(param.src, this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(
+					height))
+				if (borderWidth && borderWidth > 0) {
+					ctx.setStrokeStyle(borderColor);
+					ctx.setLineWidth(this.getPX(borderWidth));
+					ctx.stroke();
+				}
+				ctx.restore();
+			},
+			startDrawRect(ctx, param) {
+				let styles = param.style || {}
+				let {
+					width,
+					height,
+					left,
+					top,
+					borderWidth,
+					backgroundColor,
+					gradientColor,
+					gradientType = 1,
+					borderColor,
+					borderRadius = 0,
+					opacity = 1,
+					shadow
+				} = styles;
+				if (backgroundColor) {
+					ctx.save();
+					ctx.setGlobalAlpha(opacity);
+					if (gradientColor) {
+						let grd = null;
+						if (gradientType == 1) {
+							grd = ctx.createLinearGradient(0, 0, this.getPX(width), this.getPX(height))
+						} else {
+							grd = ctx.createLinearGradient(0, this.getPX(width), this.getPX(height), 0)
+						}
+						grd.addColorStop(0, backgroundColor)
+						grd.addColorStop(1, gradientColor)
+						ctx.setFillStyle(grd);
+					} else {
+						ctx.setFillStyle(backgroundColor);
+					}
+
+					if (shadow) {
+						const {
+							offsetX,
+							offsetY,
+							blur,
+							color
+						} = shadow;
+						ctx.shadowOffsetX = this.getPX(offsetX)
+						ctx.shadowOffsetY = this.getPX(offsetY)
+						ctx.shadowBlur = blur
+						ctx.shadowColor = color
+					}
+
+					if (borderRadius > 0) {
+						this.drawRadiusRect(ctx, styles);
+						ctx.fill();
+					} else {
+						ctx.fillRect(this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(height));
+					}
+					ctx.restore();
+				}
+				if (borderWidth) {
+					ctx.save();
+					ctx.setGlobalAlpha(opacity);
+					ctx.setStrokeStyle(borderColor);
+					ctx.setLineWidth(this.getPX(borderWidth));
+					if (borderRadius > 0) {
+						this.drawRadiusRect(ctx, styles);
+						ctx.stroke();
+					} else {
+						ctx.strokeRect(this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(height));
+					}
+					ctx.restore();
+				}
+
+			},
+			startDrawLine(ctx, param) {
+				let styles = param.style
+				let {
+					left,
+					top,
+					endLeft,
+					endTop,
+					color,
+					width = 1
+				} = styles;
+				ctx.save();
+				ctx.beginPath();
+				ctx.setStrokeStyle(color);
+				ctx.setLineWidth(this.getPX(width));
+				ctx.moveTo(this.getPX(left), this.getPX(top));
+				ctx.lineTo(this.getPX(endLeft), this.getPX(endTop));
+				ctx.stroke();
+				ctx.closePath();
+				ctx.restore();
+			},
+			judgeIosPermissionPhotoLibrary() {
+				// #ifdef APP-PLUS
+				var result = 0;
+				var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
+				var authStatus = PHPhotoLibrary.authorizationStatus();
+				if (authStatus === 0) {
+					result = -1;
+				} else if (authStatus == 3) {
+					result = 1;
+					console.log("相册权限已经开启");
+				} else {
+					result = 0;
+					console.log("相册权限没有开启");
+				}
+				plus.ios.deleteObject(PHPhotoLibrary);
+				return result;
+				// #endif
+			},
+			requestAndroidPermission(permissionID) {
+				// #ifdef APP-PLUS
+				return new Promise((resolve, reject) => {
+					plus.android.requestPermissions(
+						[permissionID],
+						function(resultObj) {
+							var result = 0;
+							for (var i = 0; i < resultObj.granted.length; i++) {
+								var grantedPermission = resultObj.granted[i];
+								result = 1
+							}
+							for (var i = 0; i < resultObj.deniedPresent.length; i++) {
+								var deniedPresentPermission = resultObj.deniedPresent[i];
+								result = 0
+							}
+							for (var i = 0; i < resultObj.deniedAlways.length; i++) {
+								var deniedAlwaysPermission = resultObj.deniedAlways[i];
+								result = -1
+							}
+							resolve(result);
+						},
+						function(error) {
+							resolve({
+								code: error.code,
+								message: error.message
+							});
+						}
+					);
+				});
+				// #endif
+			},
+			gotoAppPermissionSetting(isAndroid) {
+				// #ifdef APP-PLUS
+				if (!isAndroid) {
+					var UIApplication = plus.ios.import("UIApplication");
+					var application2 = UIApplication.sharedApplication();
+					var NSURL2 = plus.ios.import("NSURL");
+					var setting2 = NSURL2.URLWithString("app-settings:");
+					application2.openURL(setting2);
+
+					plus.ios.deleteObject(setting2);
+					plus.ios.deleteObject(NSURL2);
+					plus.ios.deleteObject(application2);
+				} else {
+					var Intent = plus.android.importClass("android.content.Intent");
+					var Settings = plus.android.importClass("android.provider.Settings");
+					var Uri = plus.android.importClass("android.net.Uri");
+					var mainActivity = plus.android.runtimeMainActivity();
+					var intent = new Intent();
+					intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+					var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
+					intent.setData(uri);
+					mainActivity.startActivity(intent);
+				}
+				// #endif
+			},
+			judgePermissionPhotoLibrary: async function(callback) {
+				// #ifndef APP-PLUS || MP-WEIXIN || MP-QQ
+				callback && callback(true)
+				// #endif
+
+				// #ifdef APP-PLUS
+				const res = uni.getSystemInfoSync();
+				let result;
+				let isAndroid = res.platform.toLocaleLowerCase() == "android";
+				if (isAndroid) {
+					result = await this.requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE')
+				} else {
+					result = this.judgeIosPermissionPhotoLibrary()
+				}
+				if (result == 1) {
+					callback && callback(true)
+				} else {
+					if (!(!isAndroid && result == -1)) {
+						uni.showModal({
+							title: '提示',
+							content: '您还没有开启相册权限,是否立即开启?',
+							showCancel: true,
+							success: (res) => {
+								if (res.confirm) {
+									this.gotoAppPermissionSetting(isAndroid)
+								}
+							}
+						})
+					} else {
+						callback && callback(true)
+					}
+				}
+				// #endif
+
+				// #ifdef MP-WEIXIN || MP-QQ
+				uni.authorize({
+					scope: 'scope.writePhotosAlbum',
+					success() {
+						callback && callback(true)
+					},
+					fail() {
+						uni.showModal({
+							title: '提示',
+							content: '您还没有开启相册权限,是否立即开启?',
+							showCancel: true,
+							success: (res) => {
+								if (res.confirm) {
+									wx.openSetting({
+										success(res) {}
+									});
+								}
+							}
+						})
+					}
+				})
+				// #endif
+			},
+			imgDownload(url) {
+				return new Promise((resolve, reject) => {
+					uni.downloadFile({
+						url: url,
+						success: res => {
+							resolve(res.tempFilePath);
+						},
+						fail: err => {
+							reject(false)
+						}
+					})
+				})
+			},
+			base64ToImg(base64) {
+				const uniqueId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
+				return new Promise((resolve, reject) => {
+					// #ifdef MP-WEIXIN
+					const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
+					let arrayBuffer = wx.base64ToArrayBuffer(bodyData)
+					const filePath = `${wx.env.USER_DATA_PATH}/${uniqueId}.${format}`;
+					wx.getFileSystemManager().writeFile({
+						filePath,
+						data: arrayBuffer,
+						encoding: 'binary',
+						success() {
+							resolve(filePath);
+						},
+						fail() {
+							reject(false)
+						}
+					})
+					// #endif
+					// #ifdef APP-PLUS
+					let bitmap = new plus.nativeObj.Bitmap(uniqueId);
+					bitmap.loadBase64Data(base64, function() {
+						bitmap.save(`_doc/${uniqueId}.png`, {}, function(e) {
+							let target = e.target;
+							resolve(target);
+						}, function(e) {
+							reject(false)
+						});
+					}, function() {
+						reject(false)
+					});
+					// #endif
+
+					// #ifdef H5
+					resolve(base64);
+					// #endif
+
+					// #ifndef MP-WEIXIN || APP-PLUS || H5
+					reject(false)
+					// #endif
+				})
+			},
+			startDraw(data, callback) {
+				let ctx = this.ctx
+				if (ctx) {
+					ctx.clearRect(0, 0, this.cv_width, this.cv_height)
+					data.forEach((item) => {
+						if (item.type === 'image') {
+							this.startDrawImage(ctx, item)
+						} else if (item.type === 'text') {
+							this.startDrawText(ctx, item)
+						} else if (item.type === 'rect') {
+							this.startDrawRect(ctx, item)
+						} else if (item.type === 'line') {
+							this.startDrawLine(ctx, item)
+						}
+					});
+					const platform = uni.getSystemInfoSync().platform;
+					let time = 80;
+					if (platform === 'android') {
+						time = 300;
+					}
+					setTimeout(() => {
+						ctx.draw(false, () => {
+							setTimeout(() => {
+								// #ifdef MP-ALIPAY
+								ctx.toTempFilePath({
+									success: res => {
+										callback && callback(res.apFilePath)
+									},
+									fail: err => {
+										callback && callback(false)
+									}
+								});
+								// #endif
+								// #ifndef MP-ALIPAY
+								uni.canvasToTempFilePath({
+									x: 0,
+									y: 0,
+									canvasId: this.posterId,
+									fileType: 'png',
+									quality: 1,
+									success: function(res) {
+										callback && callback(res.tempFilePath)
+									},
+									fail() {
+										callback && callback(false)
+									}
+								}, this)
+								// #endif
+							}, time)
+						})
+					}, 50)
+				} else {
+					callback && callback(false)
+				}
+			},
+			draw(data, callback) {
+				// text(文本)、image(图片)、rect(矩形),line(线条)
+				// {
+				// 	type:'image',
+				//  src:'',
+				// 	style:{
+
+				// 	}
+				// }
+				if (!data || data.length === 0) return;
+				let func = [],
+					idxes = [];
+				data.forEach((item, index) => {
+					if (item.type === 'image') {
+						//图片类型:1-本地图片(需要平台支持);2-网络图片; 3- base64 图片(仅App,微信小程序,H5支持)
+						if (item.imgType == 2) {
+							func.push(this.imgDownload(item.src))
+							idxes.push(index)
+						}
+						// #ifdef APP-PLUS || H5 || MP-WEIXIN
+						if (item.imgType == 3) {
+							func.push(this.base64ToImg(item.src))
+							idxes.push(index)
+						}
+						// #endif
+					}
+				})
+				if (func.length > 0) {
+					Promise.all(func).then(res => {
+						res.forEach((imgRes, idx) => {
+							let item = data[idxes[idx]]
+							item.src = imgRes
+						})
+						this.startDraw(data, callback)
+
+					}).catch(err => {
+						console.log(err)
+						this.toast('图片处理失败!')
+					})
+				} else {
+					this.startDraw(data, callback)
+				}
+			},
+			save(file) {
+				// #ifdef H5
+				//H5无法直接保存到相册,预览后长按保存
+				uni.previewImage({
+					urls: [file]
+				});
+				// #endif
+				// #ifndef H5
+				this.judgePermissionPhotoLibrary((res) => {
+					if (res) {
+						uni.saveImageToPhotosAlbum({
+							filePath: file,
+							success: (res) => {
+								this.toast('图片已保存到相册')
+							},
+							fail: (res) => {
+								this.toast('图片保存失败')
+							}
+						})
+					}
+				})
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-poster__cv {
+		position: fixed;
+		left: -9999px;
+		bottom: 0;
+	}
+</style>

+ 135 - 0
components/thorui/tui-progress/tui-progress.vue

@@ -0,0 +1,135 @@
+<template>
+	<view class="tui-progress__box">
+		<view class="tui-progressbar__bg"
+			:style="{ height: width + 'rpx', borderRadius: radius, background: backgroundColor }">
+			<view class="tui-progress__bar"
+				:style="{ height: width + 'rpx', background: activeColor ,transform:`translate3d(-${translateX},0,0)`,transitionDuration:`${time}s`}">
+			</view>
+		</view>
+		<view class="tui-progress__percent"
+			:style="{ width: percentWidth + 'rpx', fontSize: size + 'rpx', color: color }" v-if="showInfo">
+			{{ percentage }}%
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiProgress',
+		emits: ['activeend'],
+		props: {
+			//百分比 0-100
+			percent: {
+				type: [Number, String],
+				default: 0
+			},
+			//右侧是否显示百分比
+			showInfo: {
+				type: Boolean,
+				default: false
+			},
+			//圆角大小
+			radius: {
+				type: String,
+				default: '8rpx'
+			},
+			//右侧百分比字体大小 rpx
+			size: {
+				type: Number,
+				default: 28
+			},
+			//右侧百分比颜色
+			color: {
+				type: String,
+				default: '#333'
+			},
+			//右侧百分比宽度
+			percentWidth: {
+				type: Number,
+				default: 96
+			},
+			//进度条线条宽度 rpx
+			width: {
+				type: Number,
+				default: 8
+			},
+			//已选进度条颜色,可渐变
+			activeColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//未选择的进度条的颜色
+			backgroundColor: {
+				type: String,
+				default: '#EBEBEB'
+			},
+			//进度增加1%所需毫秒数
+			duration: {
+				type: Number,
+				default: 15
+			}
+		},
+		watch: {
+			percent(val) {
+				this.darwProgress();
+			}
+		},
+		mounted() {
+			this.darwProgress();
+		},
+		data() {
+			return {
+				percentage: 0,
+				translateX: '-100%',
+				time: 0
+			};
+		},
+		methods: {
+			darwProgress() {
+				let percent = Number(this.percent);
+				percent = percent > 100 ? 100 : percent;
+				this.time = this.duration * Math.abs(percent - this.percentage) / 1000
+				if (percent < this.percentage && (this.percentage - percent) > 30) {
+					//后百分比数大于30时 时间缩短
+					this.time = this.time / 2
+				}
+				setTimeout(() => {
+					this.$emit('activeend', {});
+				}, this.time)
+				this.percentage =percent;
+				this.translateX = (100 - percent) + '%';
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-progress__box {
+		width: 100%;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-progressbar__bg {
+		width: 100%;
+		position: relative;
+		overflow: hidden;
+		transform: translateZ(0);
+	}
+
+	.tui-progress__bar {
+		width: 100%;
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 2;
+		transform: translateX(-100%);
+		transition-delay: 0s;
+		transition-property: transform;
+		transition-timing-function: linear;
+	}
+
+	.tui-progress__percent {
+		text-align: center;
+	}
+</style>

+ 92 - 0
components/thorui/tui-radio-group/tui-radio-group.vue

@@ -0,0 +1,92 @@
+<template>
+	<!-- #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-TOUTIAO -->
+	<radio-group :name="name">
+		<slot></slot>
+	</radio-group>
+	<!-- #endif -->
+
+	<!-- #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ -->
+	<tui-form-field :name="name" :value="val">
+		<slot></slot>
+	</tui-form-field>
+	<!-- #endif -->
+</template>
+
+<script>
+	export default {
+		name: "tui-radio-group",
+		emits: ['change', 'input', 'update:modelValue'],
+		// #ifndef VUE3
+		// #ifdef MP-WEIXIN
+		behaviors: ['wx://form-field-group'],
+		// #endif
+		// #endif
+		props: {
+			name: {
+				type: String,
+				default: ''
+			},
+			// #ifdef VUE3
+			modelValue: {
+				type: String,
+				default: ''
+			},
+			// #endif
+			value: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				val: ''
+			}
+		},
+		watch: {
+			// #ifdef VUE3
+			modelValue(val) {
+				this.modelChange(val)
+			},
+			// #endif
+			value(val) {
+				this.modelChange(val)
+			}
+		},
+		created() {
+			this.childrens = []
+		},
+		methods: {
+			radioChange(e) {
+				this.$emit('change', e)
+				this.$emit('input', e.detail.value)
+				// #ifdef VUE3
+				this.$emit("update:modelValue", e.detail.value);
+				// #endif
+			},
+			changeValue(value, target) {
+				this.val = value;
+				this.childrens.forEach(item => {
+					if (item !== target) {
+						item.val = false;
+					}
+				})
+				let e = {
+					detail: {
+						value: value
+					}
+				}
+				this.radioChange(e)
+			},
+			modelChange(value) {
+				this.childrens.forEach(item => {
+					if (item.value === value) {
+						item.val = true;
+					} else {
+						item.val = false;
+					}
+				})
+			}
+		}
+	}
+</script>
+<style></style>

+ 200 - 0
components/thorui/tui-radio/tui-radio.vue

@@ -0,0 +1,200 @@
+<template>
+	<view class="tui-checkbox__input" :class="{'tui-checkbox__disabled':disabled}"
+		:style="{backgroundColor:getBackgroundStyle(val,isCheckMark),border:getBorderStyle(val,isCheckMark),zoom:nvue?1:scaleRatio,transform:`scale(${nvue?scaleRatio:1})`}"
+		@tap.stop="radioChange">
+		<view class="tui-check__mark" :style="{borderBottomColor:checkMarkColor,borderRightColor:checkMarkColor}"
+			v-if="val"></view>
+		<radio class="tui-radio__hidden" style="position: absolute;opacity: 0;" hidden :color="color" :disabled="disabled" :value="value" :checked="val"></radio>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-radio",
+		emits: ['change'],
+		options: {
+			virtualHost: true
+		},
+		props: {
+			value: {
+				type: String,
+				default: ''
+			},
+			checked: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//radio选中背景颜色
+			color: {
+				type: String,
+				default: '#5677fc'
+			},
+			//radio未选中时边框颜色
+			borderColor: {
+				type: String,
+				default: '#ccc'
+			},
+			//是否只展示对号,无边框背景
+			isCheckMark: {
+				type: Boolean,
+				default: false
+			},
+			//对号颜色
+			checkMarkColor: {
+				type: String,
+				default: '#fff'
+			},
+			scaleRatio: {
+				type: [Number, String],
+				default: 1
+			}
+		},
+		created() {
+			this.val = this.checked;
+			this.group = this.getParent()
+			if (this.group) {
+				this.group.childrens.push(this);
+				if (this.group.value) {
+					this.val = this.value === this.group.value
+				}
+				// #ifdef VUE3
+				if (this.group.modelValue) {
+					this.val = this.value === this.group.modelValue
+				}
+				// #endif
+			}
+			this.label = this.getParent('tui-label')
+			if (this.label) {
+				this.label.childrens.push(this);
+			}
+		},
+		watch: {
+			checked(newVal) {
+				this.val = newVal;
+			},
+			val(newVal) {
+				if (newVal && this.group) {
+					this.group.changeValue(this.value, this);
+				}
+			}
+		},
+		data() {
+			let nvue = false;
+			// #ifdef APP-NVUE
+			nvue = true;
+			// #endif
+			return {
+				val: false,
+				nvue: nvue
+			};
+		},
+		methods: {
+			getBackgroundStyle(val, isCheckMark) {
+				let color = val ? this.color : '#fff'
+				if (isCheckMark) {
+					color = 'transparent'
+				}
+				return color;
+			},
+			getBorderStyle(val, isCheckMark) {
+				let color = val ? this.color : this.borderColor;
+				if (isCheckMark) {
+					color = 'transparent'
+				}
+				return `1px solid ${color}`;
+			},
+			radioChange(e) {
+				if (this.disabled || this.val) return;
+				this.val = true;
+				this.$emit('change', {
+					checked: this.val,
+					value: this.value
+				})
+			},
+			getParent(name = 'tui-radio-group') {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== name) {
+					parent = parent.$parent;
+					if (!parent) return false;
+					parentName = parent.$options.name;
+				}
+				return parent;
+			},
+			labelClick() {
+				this.radioChange()
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-checkbox__input {
+		position: relative;
+		width: 40rpx;
+		height: 40rpx;
+		border-width: 1px;
+		border-style: solid;
+		/* #ifdef APP-NVUE */
+		border-radius: 40rpx;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		border-radius: 50%;
+		display: inline-flex;
+		box-sizing: border-box;
+		vertical-align: top;
+		flex-shrink: 0;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		overflow: hidden;
+		font-size: 0;
+		color: rgba(0, 0, 0, 0);
+	}
+
+	.tui-check__mark {
+		width: 20rpx;
+		height: 40rpx;
+		border-bottom-style: solid;
+		border-bottom-width: 3px;
+		border-bottom-color: #FFFFFF;
+		border-right-style: solid;
+		border-right-width: 3px;
+		border-right-color: #FFFFFF;
+		transform: rotate(45deg) scale(0.5);
+		transform-origin: 54% 48%;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.tui-radio__hidden {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		height: 100%;
+		border: 0 none;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 100wx;
+		height: 100wx;
+		border-width: 0;
+		/* #endif */
+		position: absolute;
+		top: 0;
+		left: 0;
+		opacity: 0;
+		z-index: 2;
+	}
+
+	.tui-checkbox__disabled {
+		opacity: 0.6;
+	}
+</style>

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

@@ -0,0 +1,179 @@
+<template>
+	<view class="tui-rate-box" @touchmove="touchMove">
+		<block v-for="(item, index) in numbers" :key="index">
+			<view class="tui-icon"
+				:class="['tui-relative','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" :class="['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',
+		emits: ['change'],
+		props: {
+			//数量
+			quantity: {
+				type: [Number,String],
+				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,
+				numbers:[]
+			};
+		},
+		created() {
+			this.handleQuantity(this.quantity)
+			this.percent = Number(this.score || 0) * 100;
+		},
+		watch: {
+			score(newVal, oldVal) {
+				this.percent = Number(newVal || 0) * 100;
+			},
+			quantity(val){
+				this.handleQuantity(val)
+			}
+		},
+		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.quantity ? this.quantity : index;
+				this.$emit('change', {
+					index: index,
+					params: this.params
+				});
+			},
+			handleQuantity(quantity){
+				quantity = Number(quantity) || 5
+				quantity = Math.ceil(quantity)
+				this.numbers = Array.from(new Array(quantity + 1).keys()).slice(1)
+			}
+		},
+		mounted() {
+			setTimeout(()=>{
+				const className = '.tui-rate-box';
+				let query = uni.createSelectorQuery().in(this);
+				query
+					.select(className)
+					.boundingClientRect(res => {
+						this.pageX = res.left || 0;
+					})
+					.exec();
+			},80)
+		}
+	};
+</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: inline-flex;
+		align-items: center;
+		margin: 0;
+		padding: 0;
+	}
+</style>

+ 156 - 0
components/thorui/tui-roll-news/tui-roll-news.vue

@@ -0,0 +1,156 @@
+<template>
+	<view class="tui-roll-news"
+		:style="{background:background,width:!width?'100%':width+'rpx',padding:padding,borderRadius:radius+'rpx'}">
+		<view class="tui-notice__shrink" @tap.stop="leftClick">
+			<slot></slot>
+		</view>
+		<swiper :vertical="vertical" :autoplay="true" :circular="true" :interval="interval"
+			class="tui-roll__news-swiper" :style="{height:height+'rpx'}">
+			<swiper-item v-for="(item,index) in list" :key="index" class="tui-roll__news-item">
+				<text class="tui-rollnews__text"
+					:style="{color:color,fontSize:size+'rpx',fontWeight:bold?'bold':'',lines:lines,'-webkit-line-clamp':lines}"
+					@tap="onClick(index)">{{item[prop] || ''}}</text>
+			</swiper-item>
+		</swiper>
+		<view class="tui-notice__shrink" @tap.stop="rightClick">
+			<slot name="right"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-roll-news",
+		emits: ['click', 'leftClick', 'rightClick'],
+		props: {
+			list: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//list 显示key
+			prop: {
+				type: String,
+				default: 'title'
+			},
+			width: {
+				type: [Number, String],
+				default: 0
+			},
+			//滚动消息高度
+			height: {
+				type: [Number, String],
+				default: 80
+			},
+			//字体大小 rpx
+			size: {
+				type: [Number, String],
+				default: 28
+			},
+			color: {
+				type: String,
+				default: '#333'
+			},
+			bold: {
+				type: Boolean,
+				default: false
+			},
+			background: {
+				type: String,
+				default: '#fff'
+			},
+			radius: {
+				type: [Number, String],
+				default: 0
+			},
+			padding: {
+				type: String,
+				default: '0 30rpx'
+			},
+			//内容显示行数,超出隐藏
+			lines: {
+				type: Number,
+				default: 1
+			},
+			//自动切换时间间隔(毫秒)
+			interval: {
+				type: Number,
+				default: 3000
+			},
+			//是否纵向滚动切换内容
+			vertical: {
+				type: Boolean,
+				default: true
+			}
+		},
+		data() {
+			return {
+
+			};
+		},
+		methods: {
+			onClick(index) {
+				const item = this.list[index]
+				this.$emit('click', {
+					index: index,
+					item: item
+				})
+			},
+			leftClick() {
+				this.$emit('leftClick', {})
+			},
+			rightClick() {
+				this.$emit('rightClick', {})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-roll-news {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		overflow: hidden;
+	}
+
+	.tui-notice__shrink {
+		/* #ifndef APP-NVUE */
+		flex-shrink: 0;
+		display: flex;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-roll__news-swiper {
+		flex: 1;
+	}
+
+	.tui-roll__news-item {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		display: flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.tui-rollnews__text {
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		overflow: hidden;
+		word-break: break-all;
+		white-space: normal;
+		-webkit-box-orient: vertical;
+		/* #endif */
+		overflow: hidden;
+	}
+</style>

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

@@ -0,0 +1,296 @@
+<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);
+				//当start大于当前percentage时,start设置为0
+				start = start > percentage ? 0 : start;
+				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 {
+							setTimeout(() => {
+								cancelAnimationFrame(requestId)
+								requestId = null;
+								renderLoop = null;
+							}, 20)
+						}
+					})
+				}
+				renderLoop()
+				// 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);
+					}
+					let isEnd = percentage === 0 || (that.bool(res.counterclockwise) && startPercentage === 100);
+					if (!isEnd) {
+						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)
+						})
+						callback && callback(false)
+					} else {
+						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',
+		emits: ['change','end'],
+		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>

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

@@ -0,0 +1,179 @@
+<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',
+	emits: ['index','share'],
+	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>

+ 136 - 0
components/thorui/tui-section/tui-section.vue

@@ -0,0 +1,136 @@
+<template>
+	<view class="tui-section" :style="{margin:margin,background:background,padding:padding}">
+		<view class="tui-section__title" @tap="handleClick">
+			<view class="tui-section__decorate" v-if="isLine"
+				:style="{background:lineColor,width:lineWidth+'rpx',left:`-${lineRight}rpx`,top:lineGap+'rpx',bottom:lineGap+'rpx',borderRadius:lineCap==='circle'?`${lineWidth}rpx`:0}">
+			</view>
+			<slot name="left"></slot>
+			<text :style="{fontSize:size+'rpx',color:color,fontWeight:fontWeight}" v-if="title">{{title}}</text>
+			<slot></slot>
+		</view>
+		<view class="tui-section__sub" :style="{paddingTop:descrTop+'rpx'}" v-if="descr">
+			<text class="tui-section__descr" :style="{fontSize:descrSize+'rpx',color:descrColor}"
+				>{{descr}}</text>
+		</view>
+		<slot name="descr"></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiSection",
+		emits: ['click'],
+		props: {
+			title: {
+				type: String,
+				default: ''
+			},
+			size: {
+				type: [Number, String],
+				default: 32
+			},
+			color: {
+				type: String,
+				default: '#333'
+			},
+			fontWeight: {
+				type: [Number, String],
+				default: 400
+			},
+			descr: {
+				type: String,
+				default: ''
+			},
+			descrSize: {
+				type: [Number, String],
+				default: 24
+			},
+			descrColor: {
+				type: String,
+				default: '#7a7a7a'
+			},
+			descrTop: {
+				type: [Number, String],
+				default: 16
+			},
+			isLine: {
+				type: Boolean,
+				default: false
+			},
+			lineWidth: {
+				type: [Number, String],
+				default: 8
+			},
+			lineColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			//square、circle
+			lineCap: {
+				type: String,
+				default: 'circle'
+			},
+			lineRight: {
+				type: [Number, String],
+				default: 16
+			},
+			lineGap: {
+				type: [Number, String],
+				default: 4
+			},
+			background: {
+				type: String,
+				default: 'transparent'
+			},
+			padding: {
+				type: String,
+				default: '20rpx 30rpx'
+			},
+			margin: {
+				type: String,
+				default: '0'
+			}
+		},
+		methods: {
+			handleClick() {
+				this.$emit('click', {
+					title: this.title
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-section {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.tui-section__title {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		word-break: break-all;
+		flex-shrink: 0;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.tui-section__decorate {
+		position: absolute;
+	}
+
+	.tui-section__sub {
+		/* #ifndef APP-NVUE */
+		word-break: break-all;
+		/* #endif */
+	}
+
+	.tui-section__descr {
+		font-weight: 400;
+	}
+</style>

+ 501 - 0
components/thorui/tui-select/tui-select.vue

@@ -0,0 +1,501 @@
+<template>
+	<view @touchmove.stop.prevent="stop">
+		<view class="tui-select--mask" :class="{'tui-select--mask-show':show}" :style="getStyles" @tap.stop="maskClose">
+		</view>
+		<view class="tui-select--wrap" :class="{'tui-select--wrap-show':show}"
+			:style="{borderTopLeftRadius:radius+'rpx',borderTopRightRadius:radius+'rpx',background:background,zIndex:zIndex}">
+			<view class="tui-select--header"
+				:style="{background:background,borderTopLeftRadius:radius+'rpx',borderTopRightRadius:radius+'rpx'}">
+				<text class="tui-select--header-text"
+					:style="{fontSize:titleSize+'rpx',color:titleColor,fontWeight:fontWeight}">{{title}}</text>
+				<view class="tui-select--header-close" @tap.stop="handleClose">
+					<icon type="clear" color="#ccc" :size="16"></icon>
+				</view>
+				<view class="tui-select--header-line" :style="{background:dividerColor}"></view>
+			</view>
+			<scroll-view scroll-y class="tui-select--scroll" :show-scrollbar="false" :style="{height:height+'rpx'}">
+				<view class="tui-select--list">
+					<view class="tui-select--item" :style="{padding:padding}" @tap="itemClick(index)"
+						v-for="(model,index) in itemList" :key="index"
+						:class="{'tui-select--reverse':reverse,'tui-select--item-active':highlight}">
+						<view class="tui-select--checkbox" :class="{'tui-select--is-checkmark ':isCheckMark}"
+							:style="{background:model.checked && !isCheckMark ?checkboxColor:'transparent',borderColor:model.checked && !isCheckMark ?checkboxColor:borderColor}">
+							<view class="tui-select--checkmark"
+								:style="{borderBottomColor:checkmarkColor,borderRightColor:checkmarkColor}"
+								v-if="model.checked"></view>
+						</view>
+						<view class="tui-select--flex">
+							<view class="tui-select--icon-box"
+								:class="{'tui-select--icon-ml':!reverse,'tui-select--icon-mr':reverse}"
+								:style="{width:iconWidth+'rpx',height:iconWidth+'rpx'}" v-if="model.src">
+								<image :src="model.src" :style="{width:iconWidth+'rpx',height:iconWidth+'rpx'}"
+									mode="widthFix"></image>
+							</view>
+							<text class="tui-select--item-text"
+								:class="{'tui-select--text-pl':!reverse,'tui-select--text-pr':reverse}"
+								:style="{fontSize:size+'rpx',color:color}">{{model.text}}</text>
+						</view>
+						<view v-if="dividerLine" class="tui-select--item-line"
+							:style="{background:dividerColor,left:reverse?0:bottomLeft+'rpx',right:reverse?bottomLeft+'rpx':0}">
+						</view>
+					</view>
+				</view>
+			</scroll-view>
+			<view class="tui-select--btn-wrap">
+				<view class="tui-select--btn" :style="{background:btnBackground}">
+					<text class="tui-select--btn" :class="['tui-select--btn-text']" :style="{color:btnColor}"
+						@tap.stop="handleClick">{{btnText}}</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-select",
+		emits: ['confirm', 'close'],
+		props: {
+			show: {
+				type: Boolean,
+				default: false
+			},
+			list: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			height: {
+				type: [Number, String],
+				default: 600
+			},
+			radius: {
+				type: [Number, String],
+				default: 24
+			},
+			title: {
+				type: String,
+				default: '请选择'
+			},
+			titleSize: {
+				type: [Number, String],
+				default: 32
+			},
+			titleColor: {
+				type: String,
+				default: '#333'
+			},
+			fontWeight: {
+				type: [Number, String],
+				default: 400
+			},
+			multiple: {
+				type: Boolean,
+				default: false
+			},
+			background: {
+				type: String,
+				default: '#fff'
+			},
+			padding: {
+				type: String,
+				default: '30rpx'
+			},
+			//选择框选中后颜色
+			checkboxColor: {
+				type: String,
+				default: '#5677fc'
+			},
+			borderColor: {
+				type: String,
+				default: '#ccc'
+			},
+			isCheckMark: {
+				type: Boolean,
+				default: false
+			},
+			checkmarkColor: {
+				type: String,
+				default: '#fff'
+			},
+			reverse: {
+				type: Boolean,
+				default: false
+			},
+			dividerLine: {
+				type: Boolean,
+				default: true
+			},
+			dividerColor: {
+				type: String,
+				default: '#EEEEEE'
+			},
+			bottomLeft: {
+				type: [Number, String],
+				default: 30
+			},
+			highlight: {
+				type: Boolean,
+				default: true
+			},
+			iconWidth: {
+				type: [Number, String],
+				default: 48
+			},
+			size: {
+				type: [Number, String],
+				default: 30
+			},
+			color: {
+				type: String,
+				default: '#333'
+			},
+			btnText: {
+				type: String,
+				default: '确定'
+			},
+			btnBackground: {
+				type: String,
+				default: '#5677fc'
+			},
+			btnColor: {
+				type: String,
+				default: '#fff'
+			},
+			maskBackground: {
+				type: String,
+				default: 'rgba(0,0,0,.6)'
+			},
+			maskClosable: {
+				type: [Boolean,String],
+				default: false
+			},
+			zIndex: {
+				type: [Number, String],
+				default: 1000
+			}
+		},
+		computed: {
+			getStyles() {
+				return `background:${this.maskBackground};z-index:${Number(this.zIndex)-1};`
+			}
+		},
+		watch: {
+			list(newVal) {
+				this.initData(newVal)
+			}
+		},
+		data() {
+			return {
+				itemList: [],
+				index: -1
+			};
+		},
+		created() {
+			this.initData(this.list)
+		},
+		methods: {
+			initData(vals) {
+				vals = JSON.parse(JSON.stringify(vals))
+				if (vals && vals.length > 0) {
+					if (typeof vals[0] !== 'object') {
+						vals = vals.map(item => {
+							return {
+								text: item,
+								checked: false
+							}
+						})
+					} else {
+						vals.map((item,index) => {
+							item.checked = item.checked || false
+							if(!this.multiple && item.checked){
+								this.index=index
+							}
+						})
+					}
+					this.itemList = vals;
+				}
+			},
+			itemClick(index) {
+				let vals = [...this.itemList]
+				let item = vals[index]
+				if (this.multiple) {
+					item.checked = !item.checked;
+				} else {
+					vals.forEach((item, idx) => {
+						if (index === idx) {
+							item.checked = true
+						} else {
+							item.checked = false
+						}
+					})
+					this.index = index
+				}
+				this.itemList = vals;
+			},
+			handleClick() {
+				if (this.multiple) {
+					let items = []
+					this.itemList.forEach((item, idx) => {
+						if (item.checked) {
+							items.push(this.list[idx])
+						}
+					})
+					this.$emit('confirm', {
+						options: items
+					})
+				} else {
+					let index = this.index;
+					this.$emit('confirm', {
+						index: index,
+						options: index === -1 ? '' : this.list[index]
+					})
+				}
+
+			},
+			maskClose() {
+				if (!this.maskClosable) return;
+				this.handleClose()
+			},
+			handleClose() {
+				this.$emit('close', {})
+			},
+			stop() {}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-select--mask {
+		position: fixed;
+		left: 0;
+		right: 0;
+		top: 0;
+		bottom: 0;
+		transition: all ease-in-out .3s;
+		visibility: hidden;
+		opacity: 0;
+	}
+
+	.tui-select--mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+
+	.tui-select--wrap {
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		width: 100%;
+		transform: translate3d(0, 100%, 0);
+		transition: all 0.3s ease-in-out;
+		min-height: 20rpx;
+		opacity: 0;
+		visibility: hidden;
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+
+	.tui-select--wrap-show {
+		transform: translate3d(0, 0, 0);
+		opacity: 1;
+		visibility: visible;
+	}
+
+	.tui-select--scroll {
+		width: 100%;
+		flex: 1;
+	}
+
+	.tui-select--list {
+		width: 100%;
+	}
+
+	.tui-select--item {
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+		position: relative;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+		position: relative;
+	}
+
+	.tui-select--item-line {
+		position: absolute;
+		bottom: 0;
+		height: 1px;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 100%;
+		z-index: 1;
+	}
+
+	.tui-select--item-active:active {
+		background: rgba(0, 0, 0, .2);
+	}
+
+	.tui-select--flex {
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.tui-select--reverse {
+		justify-content: space-between;
+		flex-direction: row-reverse;
+	}
+
+	.tui-select--checkbox {
+		font-size: 0;
+		color: rgba(0, 0, 0, 0);
+		width: 40rpx;
+		height: 40rpx;
+		border-width: 1px;
+		border-style: solid;
+		display: inline-flex;
+		box-sizing: border-box;
+		border-radius: 50%;
+		vertical-align: top;
+		flex-shrink: 0;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		overflow: hidden;
+		position: relative;
+	}
+
+	.tui-select--is-checkmark {
+		border-width: 0 !important;
+		background: transparent !important;
+	}
+
+	.tui-select--checkmark {
+		width: 20rpx;
+		height: 40rpx;
+		border-bottom-style: solid;
+		border-bottom-width: 3px;
+		border-bottom-color: #FFFFFF;
+		border-right-style: solid;
+		border-right-width: 3px;
+		border-right-color: #FFFFFF;
+		box-sizing: border-box;
+		transform: rotate(45deg) scale(0.5) translateZ(0);
+		transform-origin: 54% 48%;
+	}
+
+	.tui-select--item-text {
+		word-break: break-all;
+		font-weight: normal;
+	}
+
+	.tui-select--text-pl {
+		padding-left: 20rpx;
+	}
+
+	.tui-select--text-pr {
+		padding-right: 20rpx;
+	}
+
+	.tui-select--icon-box {
+		overflow: hidden;
+		background-color: #F8F8F8;
+		flex-shrink: 0;
+	}
+
+	.tui-select--icon-ml {
+		margin-left: 20rpx;
+	}
+
+	.tui-select--icon-mr {
+		margin-right: 20rpx;
+	}
+
+	.tui-select--header {
+		width: 100%;
+		display: flex;
+		flex: 1;
+		height: 98rpx;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		position: relative;
+
+	}
+
+	.tui-select--header-line {
+		position: absolute;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		height: 1px;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 100%;
+		z-index: 1;
+	}
+
+	.tui-select--header-text {
+		text-align: center;
+		width: 100%;
+		white-space: nowrap;
+		overflow: hidden;
+		box-sizing: border-box;
+		text-overflow: ellipsis;
+		flex: 1;
+		padding: 0 88rpx;
+	}
+
+	.tui-select--header-close {
+		width: 50rpx;
+		height: 50rpx;
+		position: absolute;
+		right: 32rpx;
+		top: 24rpx;
+		text-align: right;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-select--btn-wrap {
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		padding: 32rpx;
+	}
+
+	.tui-select--btn {
+		width: 100%;
+		display: flex;
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		height: 84rpx;
+		border-radius: 44rpx;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+		font-size: 30rpx;
+		font-weight: normal;
+		text-align: center;
+	}
+
+	.tui-select--btn-text:active {
+		background: rgba(0, 0, 0, .2);
+	}
+</style>

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

@@ -0,0 +1,263 @@
+<template>
+	<view class="tui-skeleton-cmomon tui-skeleton-box"
+		:style="{width: winWidth+'px', height:winHeight+'px', backgroundColor:backgroundColor}">
+		<view class="tui-skeleton-cmomon" :class="{'tui-skeleton__active':active}" 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: false
+			},
+			//loading类型[1-10]
+			loadingType: {
+				type: Number,
+				default: 1
+			},
+			//是否展示动画效果
+			active: {
+				type: Boolean,
+				default: true
+			}
+		},
+		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);
+		}
+	}
+
+	.tui-skeleton__active {
+		background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
+		animation: tui-active 1.4s ease infinite;
+		background-size: 400% 100%
+	}
+
+	@keyframes tui-active {
+		0% {
+			background-position: 100% 50%
+		}
+
+		100% {
+			background-position: 0 50%
+		}
+	}
+</style>

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

@@ -0,0 +1,218 @@
+<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",
+		emits: ['success'],
+		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>

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

@@ -0,0 +1,76 @@
+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;
+	var tsb = ins.selectComponent('.tui-slider-block')
+	var tsg = ins.selectComponent('.tui-slide-glided')
+	if (!tsb || !tsg) return;
+	tsb.setStyle({
+		transform: 'translate3d(' + left + 'px,0,0)'
+	})
+	tsg.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
+}

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

@@ -0,0 +1,255 @@
+<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" :class="[direction === 'column' ? 'tui-step-column_ico' : 'tui-step-row_ico']" :style="{ width: direction === 'column' ? '36rpx' : '100%' }">
+				<view v-if="!item.name && !item.icon" class="tui-step-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="item.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',
+		emits: ['click'],
+		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内", 
+						size:字体图标大小,单位px
+						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: 50%;
+		position: relative;
+		z-index: 3;
+		margin: 0 auto;
+		border-width: 1rpx;
+		border-style: solid;
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+	}
+
+	.tui-step-row_ico {
+		align-items: center;
+	}
+
+	.tui-step-column_ico {
+		align-items: flex-start;
+	}
+
+	.tui-step-line {
+		position: absolute;
+		left: 50%;
+		top: 20rpx;
+		width: 100%;
+		height: 0rpx;
+		border-top-width: 1px;
+		z-index: 2;
+		transform: translateY(-50%) translateZ(0);
+	}
+
+	.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;
+		display: flex;
+		justify-content: 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;
+		max-width: 80%;
+	}
+
+	.tui-step-column_line {
+		position: absolute;
+		height: 100%;
+		top: 0;
+		left: 18rpx;
+		margin: 0;
+		width: 0rpx;
+		border-right-width: 1px;
+		transform: none !important;
+	}
+
+	.tui-custom-left {
+		left: 20rpx !important;
+	}
+</style>

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

@@ -0,0 +1,125 @@
+<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',
+		emits: ['prop', 'change'],
+		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>

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

@@ -0,0 +1,47 @@
+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
+	}
+	var tsb = ownerInstance.selectComponent('.tui-sticky-bar')
+	var tss = ownerInstance.selectComponent('.tui-sticky-seat')
+	if (!tsb || !tss) return;
+	if (isFixed) {
+		tsb.setStyle({
+			"top": stickyTop + 'px'
+		}).addClass('tui-sticky-fixed')
+		tss.setStyle({
+			"display": 'block'
+		})
+	} else {
+		tsb.setStyle({
+			"top": 'auto'
+		}).removeClass('tui-sticky-fixed')
+		tss.setStyle({
+			"display": 'none'
+		})
+	}
+	ownerInstance.triggerEvent("sticky", [{
+		isFixed: isFixed,
+		index: parseInt(dataset.index)
+	}])
+}
+
+module.exports = {
+	stickyChange: stickyChange
+}

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

@@ -0,0 +1,164 @@
+<template>
+	<view class="tui-sticky-class" :id="tui_Id">
+		<!--sticky 容器-->
+		<view :style="{height: stickyHeight,background: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",
+		emits: ['sticky', 'change'],
+		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();
+			}, 50)
+		},
+		data() {
+			const Id = `tui_${Math.ceil(Math.random() * 10e5).toString(36)}`
+			return {
+				timer: null,
+				top: 0,
+				height: 0,
+				isFixed: false,
+				initialize: 0, //重新初始化
+				tui_Id: Id
+			};
+		},
+		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 selectId = `#${this.tui_Id}`;
+					uni.createSelectorQuery()
+						// #ifndef MP-ALIPAY
+						.in(this)
+						// #endif
+						.select(selectId)
+						.fields({
+							size: true,
+							rect: true
+						}, 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>

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

@@ -0,0 +1,315 @@
+<template>
+	<view class="tui-swipeout-wrap" :style="{ backgroundColor: backgroundColor }">
+		<view class="tui-swipeout-item" :class="[isShowBtn ? 'swipe-action-show' : '']" 
+			:style="{ transform: 'translate(' + position.pageX + 'px,0)' }">
+			<view class="tui-swipeout-content" @touchstart="handlerTouchstart"
+			@touchmove="handlerTouchmove" @touchend="handlerTouchend" @mousedown="handlerTouchstart"
+			@mousemove="handlerTouchmove" @mouseup="handlerTouchend">
+				<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>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiSwipeAction',
+		emits: ['click'],
+		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;
+				let touches = event.touches
+				if (touches && touches.length > 1) return;
+				this.move = true;
+				touches = 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;
+					}
+					if (spacing.pageX== 0) {
+						this.isShowBtn = false;
+					}
+					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>

+ 218 - 0
components/thorui/tui-switch/tui-switch.vue

@@ -0,0 +1,218 @@
+<template>
+	<view class="tui-switch__input" :style="{zoom:nvue?1:scaleRatio,transform:`scale(${nvue?scaleRatio:1})`}">
+		<switch v-if="type==='switch'" :class="{'tui-pevents':isLabel}" @change="change" :name="name" :checked="val" :disabled="disabled" :color="color">
+		</switch>
+		<view class="tui-checkbox__self" :class="{'tui-checkbox__disabled':disabled}"
+			:style="{backgroundColor:val?color:'#fff',border:val?`1px solid ${color}`:`1px solid ${borderColor}`}"
+			v-else>
+			<view class="tui-check__mark" :style="{borderBottomColor:checkMarkColor,borderRightColor:checkMarkColor}"
+				v-if="val"></view>
+			<switch class="tui-switch__hidden" :class="{'tui-pevents':isLabel}" style="position: absolute;opacity: 0;"
+				@change="change" :name="name" type="checkbox" :checked="val" :disabled="disabled"></switch>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-switch",
+		emits: ['change'],
+		// #ifndef VUE3
+		// #ifdef MP-WEIXIN
+		behaviors: ['wx://form-field-group'],
+		// #endif
+		// #ifdef MP-BAIDU || MP-QQ
+		behaviors: ['uni://form-field'],
+		// #endif
+		// #endif
+		props: {
+			//开关选择器名称
+			name: {
+				type: String,
+				default: ''
+			},
+			checked: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			//样式,有效值:switch, checkbox
+			type: {
+				type: String,
+				default: 'switch'
+			},
+			//switch选中颜色
+			color: {
+				type: String,
+				default: '#5677fc'
+			},
+			//边框颜色,type=checkbox时生效
+			borderColor: {
+				type: String,
+				default: '#ccc'
+			},
+			//对号颜色,type=checkbox时生效
+			checkMarkColor: {
+				type: String,
+				default: '#fff'
+			},
+			scaleRatio: {
+				type: [Number, String],
+				default: 1
+			}
+		},
+		data() {
+			let nvue = false;
+			// #ifdef APP-NVUE
+			nvue = true;
+			// #endif
+			return {
+				val: false,
+				nvue: nvue,
+				isLabel: false
+			};
+		},
+		watch: {
+			checked(val) {
+				this.val = val;
+			}
+		},
+		created() {
+			this.val = this.checked;
+			this.label = this.getParent();
+			if (this.label) {
+				this.isLabel = true
+				this.label.childrens.push(this)
+			}
+		},
+		methods: {
+			change(e, label) {
+				if (this.label && !label) return;
+				this.val = e.detail.value;
+				this.$emit('change', e)
+			},
+			labelClick() {
+				if (this.disabled) return;
+				let e = {
+					detail: {
+						value: !this.val
+					}
+				}
+				this.change(e, true)
+			},
+			getParent(name = 'tui-label') {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== name) {
+					parent = parent.$parent;
+					if (!parent) return false;
+					parentName = parent.$options.name;
+				}
+				return parent;
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	/* #ifndef APP-NVUE */
+	.tui-switch__input {
+		display: inline-block;
+	}
+
+	/* #endif */
+	.tui-checkbox__self {
+		font-size: 0;
+		width: 40rpx;
+		height: 40rpx;
+		/* #ifdef APP-NVUE */
+		border-radius: 40rpx;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		border-radius: 50%;
+		display: inline-flex;
+		box-sizing: border-box;
+		vertical-align: top;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		overflow: hidden;
+		position: relative;
+	}
+
+	/* #ifdef H5 || APP-VUE */
+	::v-deep .uni-switch-input {
+		margin-right: 0 !important;
+	}
+
+	/* #endif */
+
+	/* #ifdef APP-NVUE */
+	.uni-switch-input {
+		margin-right: 0 !important;
+	}
+
+	/* #endif */
+
+
+	/* #ifdef MP-WEIXIN */
+	.wx-switch-input {
+		margin-right: 0 !important;
+	}
+
+	/* #endif */
+
+	.tui-check__mark {
+		width: 20rpx;
+		height: 40rpx;
+		border-bottom-style: solid;
+		border-bottom-width: 3px;
+		border-bottom-color: #FFFFFF;
+		border-right-style: solid;
+		border-right-width: 3px;
+		border-right-color: #FFFFFF;
+		transform: rotate(45deg) scale(0.5);
+		transform-origin: 54% 48%;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.tui-switch__hidden {
+		position: absolute;
+		top: 0;
+		left: 0;
+		opacity: 0;
+		z-index: 2;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		height: 100%;
+		border: 0 none;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		/* #endif */
+
+		/* #ifdef APP-NVUE */
+		width: 100wx;
+		height: 100wx;
+		border-width: 0;
+		/* #endif */
+
+	}
+
+	/* #ifdef H5 */
+	.tui-pevents {
+		pointer-events: none;
+	}
+
+	/* #endif */
+
+	.tui-checkbox__disabled {
+		opacity: 0.6;
+	}
+</style>

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

@@ -0,0 +1,328 @@
+<template>
+	<scroll-view class="tui-scroll__view"
+		:class="[isFixed && !isSticky?'tui-tabs__fixed':'',isSticky?'tui-tabs__sticky':'',classView]"
+		: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" :class="[childClass]"
+						:style="{	color: currentTab == index ? selectedColor : color,fontSize: 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',
+		emits: ['change'],
+		options: {
+			virtualHost: true
+		},
+		props: {
+			// 标签页数据源
+			tabs: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			// 当前选项卡
+			current: {
+				type: Number,
+				default: 0
+			},
+			// 是否可以滚动
+			scroll: {
+				type: Boolean,
+				default: false
+			},
+			// tab高度 rpx
+			height: {
+				type: [Number, String],
+				default: 80
+			},
+			//组件左侧距离屏幕的间隙 单位rpx
+			leftGap: {
+				type: [Number, String],
+				default: 0
+			},
+			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(), 200);
+			},
+			/**
+			 *  监听 currentTab 变化, 做对应处理
+			 */
+			current(newVal, oldVal) {
+				this.scrollByIndex(newVal);
+			},
+			leftGap(newVal) {
+				this.gap = uni.upx2px(Number(newVal))
+			}
+		},
+		created() {
+			this.currentTab = this.current;
+		},
+		mounted() {
+			this.gap = uni.upx2px(Number(this.leftGap))
+			this.$nextTick(() => {
+				this.init()
+			})
+		},
+		data() {
+			let childClass = `tui_10_${Math.ceil(Math.random() * 10e5).toString(36)}`;
+			let classView = `tui_01_${Math.ceil(Math.random() * 10e5).toString(36)}`;
+			return {
+				childClass,
+				classView,
+				/* 未渲染数据 */
+				windowWidth: 0, // 屏幕宽度
+				tabItems: [], // 所有 tab 节点信息
+
+				/* 渲染数据 */
+				scrolling: true, // 控制 scroll-view 滚动以在异步加载数据的时候能正确获得 dom 信息
+				needTransition: false, // 下划线是否需要过渡动画
+				translateX: 0, // 下划 line 的左边距离
+				lineWidth: 0, // 下划 line 宽度
+				scrollLeft: 0, // scroll-view 左边滚动距离
+				currentTab: 0,
+				gap: -1
+			};
+		},
+		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.gap;
+					this.lineWidth = itemWidth
+				} else {
+					// 不超出滚动的情况
+					this.translateX = itemLeft - this.gap;
+					this.lineWidth = itemWidth
+				}
+			},
+			/**
+			 *  初始化函数
+			 */
+			init() {
+				const {
+					windowWidth
+				} = uni.getSystemInfoSync();
+				// this.windowWidth = windowWidth || 375
+				const query = uni.createSelectorQuery().in(this);
+				// #ifndef MP-BAIDU
+				query
+					.select('.tui-scroll__view')
+					.boundingClientRect(res => {
+						if (res) {
+							this.windowWidth = res.width || windowWidth;
+						}
+					}).selectAll(".tui-item__child").boundingClientRect((res) => {
+						this.scrolling = true;
+						this.tabItems = res;
+						this.scrollByIndex(this.currentTab, false);
+					})
+					.exec();
+				// #endif
+				
+				// #ifdef MP-BAIDU
+				query
+					.select(`.${this.classView}`)
+					.boundingClientRect(res => {
+						if (res) {
+							this.windowWidth = res.width || windowWidth;
+						}
+					}).selectAll(`.${this.childClass}`).boundingClientRect((res) => {
+						this.scrolling = true;
+						this.tabItems = res;
+						this.scrollByIndex(this.currentTab, false);
+					})
+					.exec();
+				// #endif
+				// 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;
+		position: relative;
+		z-index: 2;
+	}
+
+	.tui-item__child {
+		display: inline-block;
+	}
+
+	.tui-tabs__line {
+		position: absolute;
+		left: 0;
+		width: 0;
+		display: inline-block;
+		z-index: 1;
+	}
+
+	.tui-tabs__line.tui-transition {
+		transition: width 0.3s, transform 0.3s;
+	}
+</style>

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

@@ -0,0 +1,281 @@
+<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.hump, item.pagePath, item.verify)">
+				<view class="tui-icon-box" :class="{ 'tui-tabbar-hump': item.hump }">
+					<image :src="current == index ? 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 == index ? 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',
+		emits: ['click'],
+		props: {
+			//当前索引
+			current: {
+				type: Number,
+				default: 0
+			},
+			//字体颜色
+			color: {
+				type: String,
+				default: '#666'
+			},
+			//字体选中颜色
+			selectedColor: {
+				type: String,
+				default: '#5677FC'
+			},
+			//背景颜色
+			backgroundColor: {
+				type: String,
+				default: '#FFFFFF'
+			},
+			//是否需要中间凸起按钮
+			hump: {
+				type: Boolean,
+				default: false
+			},
+			//固定在底部
+			isFixed: {
+				type: Boolean,
+				default: true
+			},
+			//tabbar
+			// "pagePath": "/pages/my/my", 页面路径
+			// "text": "thor", 标题
+			// "iconPath": "thor_gray.png", 图标地址
+			// "selectedIconPath": "thor_active.png", 选中图标地址
+			// "hump": true, 是否为凸起图标
+			// "num": 2,   角标数量
+			// "isDot": true,  角标是否为圆点
+			// "verify": true  是否验证  (如登录)
+			tabBar: {
+				type: Array,
+				default () {
+					return [];
+				}
+			},
+			//角标字体颜色
+			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: 9999
+			}
+		},
+		watch: {
+			current() {}
+		},
+		data() {
+			return {};
+		},
+		methods: {
+			tabbarSwitch(index, hump, pagePath, verify) {
+				this.$emit('click', {
+					index: index,
+					hump: hump,
+					pagePath: pagePath,
+					verify: verify
+				});
+			}
+		}
+	};
+</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%;
+		border-top: 1px solid #b2b2b2;
+		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>

+ 65 - 0
components/thorui/tui-table/tui-table.vue

@@ -0,0 +1,65 @@
+<template>
+	<!--需要固定宽高滚动在页面层使用scroll-view即可-->
+	<view class="tui-table__box" :style="{
+			borderTop:borderTop? `${borderWidth} solid ${borderColor}`:'0',
+			borderLeft:borderLeft? `${borderWidth} solid ${borderColor}`:'0',
+			borderBottom: borderBottom ? `${borderWidth} solid ${borderColor}` : '0',
+			borderRight: borderRight ? `${borderWidth} solid ${borderColor}` : '0'
+		}">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	//字体/背景等配置
+	export default {
+		name: 'tuiTable',
+		props: {
+			//border width 不需要border传0即可
+			borderWidth: {
+				type: String,
+				default: '1rpx'
+			},
+			//border color
+			borderColor: {
+				type: String,
+				default: '#EAEEF5'
+			},
+			//是否需要上边框
+			borderTop: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要左边框
+			borderLeft: {
+				type: Boolean,
+				default: true
+			},
+			//是否需要下边框
+			borderBottom: {
+				type: Boolean,
+				default: false
+			},
+			//是否需要右边框
+			borderRight: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				width: 320
+			};
+		},
+		methods:{
+			
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-table__box {
+		font-size: 0;
+		box-sizing: border-box;
+	}
+</style>

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

@@ -0,0 +1,319 @@
+<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'
+		}" v-if="tabsWidth>0">
+		<view v-for="(item, index) in tabs" :key="index" class="tui-tabs-item"
+			:style="{ width: itemWidth,height: height + 'rpx' }" @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',
+					fontWeight: bold && currentTab == index ? 'bold' : 'normal'
+				}">
+				{{ item.name }}
+				<view :class="[item.isDot ? 'tui-badge__dot' : 'tui-tabs__badge']"
+					:style="{ color: badgeColor, backgroundColor: badgeBgColor }" v-if="item.num || item.isDot">
+					{{ item.isDot ? '' : item.num }}
+				</view>
+			</view>
+		</view>
+		<view v-if="isSlider" 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',
+		emits: ['change'],
+		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
+			},
+			isSlider: {
+				type: Boolean,
+				default: true
+			},
+			//滑块宽度
+			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
+			},
+			//角标字体颜色
+			badgeColor: {
+				type: String,
+				default: '#fff'
+			},
+			//角标背景颜色
+			badgeBgColor: {
+				type: String,
+				default: '#F74D54'
+			},
+			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;
+		overflow: visible;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.tui-tabs-disabled {
+		opacity: 0.6;
+	}
+
+	.tui-tabs-title {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		position: relative;
+		z-index: 3;
+		overflow: visible;
+	}
+
+	.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: 1;
+		transform-style: preserve-3d;
+	}
+
+	.tui-tabs__badge {
+		position: absolute;
+		font-size: 24rpx;
+		height: 32rpx;
+		min-width: 20rpx;
+		padding: 0 6rpx;
+		border-radius: 40rpx;
+		right: 0;
+		top: 0;
+		transform: translate(88%, -50%);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+		z-index: 4;
+		font-weight: normal !important;
+	}
+
+	.tui-badge__dot {
+		position: absolute;
+		height: 16rpx;
+		width: 16rpx;
+		border-radius: 50%;
+		right: -10rpx;
+		top: -10rpx;
+		z-index: 4;
+	}
+</style>

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

@@ -0,0 +1,355 @@
+<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',
+		emits: ['click'],
+		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>

+ 251 - 0
components/thorui/tui-td/tui-td.vue

@@ -0,0 +1,251 @@
+<template>
+	<view
+		class="tui-table__column"
+		:class="[
+			ellipsis ? 'tui-column__ellipsis' : 'tui-table__middle',
+			'tui-table__' + alignItems,
+			'tui-text__' + textAlign,
+			flexGrow ? 'tui-flex-grow' : '',
+			shrink ? '' : 'tui-td__shrink',
+			top ? 'tui-td__upper' : '',
+			hidden?'tui-td__hidden':''
+		]"
+		:style="{
+			width: getWidth(span, width),
+			height: height,
+			fontSize: size + 'rpx',
+			color: color,
+			fontWeight: bold ? 'bold' : 'normal',
+			backgroundColor: backgroundColor,
+			borderRight: borderRight ? `${borderWidth} solid ${borderColor}` : '0',
+			borderBottom: borderBottom ? `${borderWidth} solid ${borderColor}` : '0',
+			borderLeft: borderLeft ? `${borderWidth} solid ${borderColor}` : '0',
+			padding: padding
+		}"
+		@tap="handleClick"
+	>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+//table td
+export default {
+	name: 'tuiTd',
+	emits: ['click'],
+	options: {
+		// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现
+		virtualHost: true
+	},
+	props: {
+		//跨度
+		span: {
+			type: Number,
+			default: 24
+		},
+		//具体值或者auto
+		// #ifdef APP-PLUS || MP-WEIXIN || H5 || MP-ALIPAY
+		width: {
+			type: String,
+			default: ''
+		},
+		// #endif
+		// #ifndef APP-PLUS || MP-WEIXIN || H5 || MP-ALIPAY
+		width: {
+			type: String,
+			default: '230rpx'
+		},
+		// #endif
+		height: {
+			type: String,
+			default: 'auto'
+		},
+		size: {
+			type: Number,
+			default: 30
+		},
+		color: {
+			type: String,
+			default: '#333'
+		},
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		backgroundColor: {
+			type: String,
+			default: 'transparent'
+		},
+		//border-right width
+		borderWidth: {
+			type: String,
+			default: '1rpx'
+		},
+		//边框颜色
+		borderColor: {
+			type: String,
+			default: '#EAEEF5'
+		},
+		//是否需要右边框
+		borderRight: {
+			type: Boolean,
+			default: true
+		},
+		//是否需要下边框
+		borderBottom: {
+			type: Boolean,
+			default: false
+		},
+		//是否需要左边框
+		borderLeft: {
+			type: Boolean,
+			default: false
+		},
+		//文字超出隐藏
+		ellipsis: {
+			type: Boolean,
+			default: false
+		},
+		padding: {
+			type: String,
+			default: '12rpx'
+		},
+		//排列:left,center,right
+		alignItems: {
+			type: String,
+			default: 'left'
+		},
+		//文本对齐:left,center,right
+		textAlign: {
+			type: String,
+			default: 'left'
+		},
+		//是否收缩
+		shrink: {
+			type: Boolean,
+			default: true
+		},
+		//铺满剩余空间
+		flexGrow: {
+			type: Boolean,
+			default: false
+		},
+		//顶部,上面;
+		top: {
+			type: Boolean,
+			default: false
+		},
+		hidden: {
+			type: Boolean,
+			default: false
+		},
+		//索引
+		index: {
+			type: Number,
+			default: 0
+		},
+		//字段key
+		keys: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		getWidth(span, width) {
+			let w = width;
+			if (!width) {
+				w = [
+					'4.16666667%',
+					'8.33333333%',
+					'12.5%',
+					'16.66666667%',
+					'20.83333333%',
+					'25%',
+					'29.16666667%',
+					'33.33333333%',
+					'37.5%',
+					'41.66666667%',
+					'45.83333333%',
+					'50%',
+					'54.16666667%',
+					'58.33333333%',
+					'62.5%',
+					'66.66666667%',
+					'70.83333333%',
+					'75%',
+					'79.16666667%',
+					'83.33333333%',
+					'87.5%',
+					'91.66666667%',
+					'95.83333333%',
+					'100%'
+				][span - 1];
+			}
+			return w;
+		},
+		handleClick() {
+			this.$emit('click', {
+				index: this.index,
+				key: this.keys
+			});
+		}
+	}
+};
+</script>
+
+<style>
+/*栅格 24*/
+.tui-table__column {
+	display: inline-block;
+	box-sizing: border-box;
+	position: relative;
+	word-break: break-all;
+}
+
+.tui-td__shrink {
+	flex-shrink: 0;
+}
+
+.tui-flex-grow {
+	flex-grow: 1;
+}
+
+.tui-table__middle {
+	display: inline-flex;
+	align-items: center;
+}
+
+.tui-table__center {
+	justify-content: center;
+}
+
+.tui-table__right {
+	justify-content: flex-end;
+}
+
+.tui-text__center {
+	text-align: center;
+}
+
+.tui-text__right {
+	text-align: right;
+}
+
+.tui-column__ellipsis {
+	overflow: hidden;
+	white-space: nowrap !important;
+	text-overflow: ellipsis;
+}
+
+.tui-td__upper {
+	z-index: 10;
+}
+
+.tui-td__hidden {
+	visibility: hidden;
+}
+
+</style>

+ 568 - 0
components/thorui/tui-textarea/tui-textarea.vue

@@ -0,0 +1,568 @@
+<template>
+	<view :class="{'tui-textarea__border':textareaBorder}" :style="{marginTop:marginTop+'rpx'}" @tap="fieldClick">
+		<view class="tui-textarea__wrap"
+			:class="{'tui-line__left':lineLeft,'tui-border__top':!borderTop || textareaBorder,'tui-border__bottom':!borderBottom || textareaBorder,'tui-textarea__flex-start':flexStart}"
+			:style="{padding:padding,backgroundColor:backgroundColor}">
+			<!-- #ifdef APP-NVUE -->
+			<view class="tui-textarea__required" v-if="required && !flexStart">
+				<text :style="{color:requiredColor,paddingTop:'2rpx'}">*</text>
+			</view>
+			<text class="tui-textarea__required" :style="{color:requiredColor,top:requiredTop}"
+				v-if="required && flexStart">*</text>
+			<!-- #endif -->
+			<!-- #ifndef APP-NVUE -->
+			<view class="tui-textarea__required" :class="{'tui-required__flex-start':flexStart}"
+				:style="{color:requiredColor,top:flexStart?requiredTop:'50%'}" v-if="required">*</view>
+			<!-- #endif -->
+			<view class="tui-textarea__label"
+				:style="{fontSize:labelSize+'rpx',color:labelColor,minWidth:labelWidth+'rpx'}" v-if="label">
+				<text :style="{fontSize:labelSize+'rpx',color:labelColor}">{{label}}</text>
+			</view>
+			<slot name="left"></slot>
+			<view class="tui-textarea__flex-1">
+				<textarea class="tui-textarea__self" :class="{'tui-text__right':textRight}"
+					:style="{height:height,minHeight:minHeight,fontSize:size+'rpx',color:color}"
+					placeholder-class="tui-placeholder" :name="name" :value="inputVal" :placeholder="placeholder"
+					:placeholderStyle="placeholderStyl" :disabled="disabled" :cursor-spacing="cursorSpacing"
+					:maxlength="maxlength" :focus="focused" :auto-height="autoHeight" :fixed="fixed"
+					:show-confirm-bar="showConfirmBar" :cursor="cursor" :selection-start="selectionStart"
+					:selection-end="selectionEnd" :adjust-position="adjustPosition" :hold-keyboard="holdKeyboard"
+					:show-count="false" :disable-default-padding="disableDefaultPadding" @focus="onFocus" @blur="onBlur"
+					@input="onInput" @confirm="onConfirm" @linechange="onLinechange"
+					@keyboardheightchange="onKeyboardheightchange"></textarea>
+				<view class="tui-textarea__counter" :style="{fontSize:counterSize+'rpx',color:counterColor}"
+					v-if="isCounter">
+					<text
+						:style="{fontSize:counterSize+'rpx',color:counterColor}">{{maxlength!=-1?`${count}/${maxlength}`:count}}</text>
+				</view>
+			</view>
+			<slot name="right"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tui-textarea",
+		emits: ['input', 'update:modelValue', 'focus', 'blur', 'confirm', 'click', 'linechange', 'keyboardheightchange'],
+		//这里加group是为了避免在表单中使用时给组件加value属性
+		// #ifndef VUE3
+		// #ifdef MP-WEIXIN
+		//加group是为了避免在表单中使用时给组件加value属性
+		behaviors: ['wx://form-field-group'],
+		// #endif
+		// #ifdef MP-BAIDU || MP-QQ
+		//如果在这些平台不需要也能识别,则删除
+		behaviors: ['uni://form-field'],
+		// #endif
+		// #endif
+		// #ifdef MP-WEIXIN
+		options: {
+			addGlobalClass: true,
+			virtualHost: true
+		},
+		// #endif
+		props: {
+			//是否为必填项
+			required: {
+				type: Boolean,
+				default: false
+			},
+			requiredColor: {
+				type: String,
+				default: '#EB0909'
+			},
+			requiredTop: {
+				type: String,
+				default: '32rpx'
+			},
+			//左侧标题
+			label: {
+				type: String,
+				default: ''
+			},
+			//标题字体大小
+			labelSize: {
+				type: Number,
+				default: 32
+			},
+			labelColor: {
+				type: String,
+				default: '#333'
+			},
+			//label 最小宽度 rpx
+			labelWidth: {
+				type: Number,
+				default: 140
+			},
+			//获取焦点
+			focus: {
+				type: Boolean,
+				default: false
+			},
+			autoHeight: {
+				type: Boolean,
+				default: false
+			},
+			fixed: {
+				type: Boolean,
+				default: false
+			},
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			placeholderStyle: {
+				type: String,
+				default: ''
+			},
+			//输入框名称
+			name: {
+				type: String,
+				default: ''
+			},
+			//输入框值
+			value: {
+				type: [Number, String],
+				default: ''
+			},
+			// #ifdef VUE3
+			//输入框值
+			modelValue: {
+				type: [Number, String],
+				default: ''
+			},
+			// #endif
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			maxlength: {
+				type: [Number, String],
+				default: 140
+			},
+			cursorSpacing: {
+				type: Number,
+				default: 0,
+			},
+			showConfirmBar: {
+				type: Boolean,
+				default: true
+			},
+			cursor: {
+				type: Number,
+				default: -1
+			},
+			selectionStart: {
+				type: Number,
+				default: -1
+			},
+			selectionEnd: {
+				type: Number,
+				default: -1
+			},
+			adjustPosition: {
+				type: Boolean,
+				default: true
+			},
+			disableDefaultPadding: {
+				type: Boolean,
+				default: true
+			},
+			holdKeyboard: {
+				type: Boolean,
+				default: false
+			},
+			height: {
+				type: String,
+				default: '200rpx'
+			},
+			minHeight: {
+				type: String,
+				default: '200rpx'
+			},
+			//标题与输入框是否顶端对齐
+			flexStart: {
+				type: Boolean,
+				default: false
+			},
+			//输入框字体大小 rpx
+			size: {
+				type: Number,
+				default: 32
+			},
+			//输入框字体颜色
+			color: {
+				type: String,
+				default: '#333'
+			},
+			// 是否显示 textarea 边框
+			textareaBorder: {
+				type: Boolean,
+				default: false
+			},
+			// 是否显示上边框
+			borderTop: {
+				type: Boolean,
+				default: true
+			},
+			// 是否显示下边框
+			borderBottom: {
+				type: Boolean,
+				default: true
+			},
+			//下边框线条是否有左偏移距离
+			lineLeft: {
+				type: Boolean,
+				default: false
+			},
+			// 是否自动去除两端的空格
+			trim: {
+				type: Boolean,
+				default: true
+			},
+			textRight: {
+				type: Boolean,
+				default: false
+			},
+			//输入框padding值
+			padding: {
+				type: String,
+				default: '26rpx 30rpx'
+			},
+			//输入框背景颜色
+			backgroundColor: {
+				type: String,
+				default: '#FFFFFF'
+			},
+			//输入框margin-top值 rpx
+			marginTop: {
+				type: Number,
+				default: 0
+			},
+			//是否显示底部输入长度计数
+			isCounter: {
+				type: Boolean,
+				default: false
+			},
+			//计数文本颜色
+			counterColor: {
+				type: String,
+				default: '#999'
+			},
+			//计数文本大小 rpx
+			counterSize: {
+				type: Number,
+				default: 28
+			}
+		},
+		data() {
+			return {
+				placeholderStyl: '',
+				count: 0,
+				focused: false,
+				inputVal: ''
+			};
+		},
+		watch: {
+			focus(val) {
+				this.$nextTick(() => {
+					this.focused = val
+				})
+			},
+			placeholderStyle() {
+				this.fieldPlaceholderStyle()
+			},
+			// #ifdef VUE3
+			modelValue(newVal) {
+				this.inputVal = newVal || ''
+				this.count = this.getCount(this.inputVal.length)
+			},
+			// #endif
+			value(newVal) {
+				this.inputVal = newVal || ''
+				this.count = this.getCount(this.inputVal.length)
+			}
+		},
+		created() {
+			// #ifndef VUE3
+			this.inputVal = this.value || ''
+			// #endif
+
+			// #ifdef VUE3
+			if (this.value && !this.modelValue) {
+				this.inputVal = this.value || ''
+			} else {
+				this.inputVal = this.modelValue || ''
+			}
+			// #endif
+			this.count = this.getCount(this.inputVal.length)
+			this.fieldPlaceholderStyle()
+		},
+		mounted() {
+			this.$nextTick(() => {
+				this.focused = this.focus
+			})
+		},
+		methods: {
+			getCount(count) {
+				const max = Number(this.maxlength)
+				if (count > max && max !== -1) {
+					return max
+				}
+				return count;
+			},
+			fieldPlaceholderStyle() {
+				if (this.placeholderStyle) {
+					this.placeholderStyl = this.placeholderStyle
+				} else {
+					const size = uni.upx2px(this.size)
+					this.placeholderStyl = `fontSize:${size}px`
+				}
+			},
+			onInput(event) {
+				let value = event.detail.value;
+				if (this.trim) value = this.trimStr(value);
+				const lenth = value.length;
+				const max = Number(this.maxlength)
+				if (lenth > max && max !== -1) {
+					lenth = max;
+					value = value.substring(0, max - 1)
+				}
+				this.count = lenth;
+				this.$emit('input', value);
+				// #ifdef VUE3
+				this.$emit('update:modelValue', value)
+				// #endif
+			},
+			onFocus(event) {
+				this.$emit('focus', event);
+			},
+			onBlur(event) {
+				this.$emit('blur', event);
+			},
+			onConfirm(e) {
+				this.$emit('confirm', e.detail.value);
+			},
+			fieldClick() {
+				this.$emit('click', {
+					name: this.name
+				});
+			},
+			onLinechange(e) {
+				this.$emit('linechange', e.detail)
+			},
+			onKeyboardheightchange(e) {
+				this.$emit('keyboardheightchange', e.detail)
+			},
+			trimStr(str) {
+				return str.replace(/^\s+|\s+$/g, '');
+			}
+		}
+	}
+</script>
+
+<style>
+	.tui-textarea__wrap {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		box-sizing: border-box;
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		flex: 1;
+		align-items: center;
+		position: relative;
+
+		/* #ifdef APP-NVUE */
+		border-top-width: 0.5px;
+		border-top-style: solid;
+		border-top-color: rgba(0, 0, 0, 0.1);
+		border-bottom-width: 0.5px;
+		border-bottom-style: solid;
+		border-bottom-color: rgba(0, 0, 0, 0.1);
+		padding: 26rpx 30rpx;
+		/* #endif */
+	}
+
+	.tui-textarea__flex-start {
+		align-items: flex-start !important;
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-textarea__wrap::before {
+		content: ' ';
+		position: absolute;
+		top: 0;
+		right: 0;
+		left: 0;
+		border-top: 1px solid var(--thorui-line-color, rgba(0, 0, 0, 0.1));
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+
+	.tui-textarea__wrap::after {
+		content: ' ';
+		position: absolute;
+		border-bottom: 1px solid var(--thorui-line-color, rgba(0, 0, 0, 0.1));
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		transform-origin: 0 100%;
+		bottom: 0;
+		right: 0;
+		left: 0;
+		z-index: 2;
+		pointer-events: none;
+	}
+
+	.tui-line__left::after {
+		left: 30rpx !important;
+	}
+
+	.tui-border__top::before {
+		border-top: 0;
+	}
+
+	.tui-border__bottom::after {
+		border-bottom: 0;
+	}
+
+	/* #endif */
+
+	/* #ifdef APP-NVUE */
+	.tui-border__top {
+		border-top-width: 0;
+	}
+
+	.tui-border__bottom {
+		border-bottom-width: 0;
+	}
+
+	/* #endif */
+	.tui-textarea__required {
+		position: absolute;
+		left: 12rpx;
+		/* #ifndef APP-NVUE */
+		height: 30rpx;
+		top: 50%;
+		transform: translateY(-50%);
+		line-height: 1.15;
+		/* #endif */
+
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		height: 100wx;
+		align-items: center;
+		justify-content: center;
+		/* #endif */
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-required__flex-start {
+		transform: translateY(0);
+	}
+
+	/* #endif */
+	.tui-textarea__label {
+		padding-right: 12rpx;
+		/* #ifndef APP-NVUE */
+		flex-shrink: 0;
+		/* #endif */
+	}
+
+	.tui-textarea__self {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		overflow: visible;
+		box-sizing: border-box;
+		/* #endif */
+
+		/* #ifdef APP-NVUE */
+		padding-top: 6px;
+		padding-bottom: 10px;
+		/* #endif */
+
+		/* #ifdef MP-ALIPAY || MP-TOUTIAO*/
+		padding-top: 0 !important;
+		padding-bottom: 0;
+		/* #endif */
+
+		/* #ifdef MP-TOUTIAO */
+		background-color: rgba(255, 255, 255, 0) !important;
+		/* #endif */
+	}
+
+	.tui-placeholder {
+		/* #ifndef APP-NVUE */
+		color: var(--thorui-text-color-placeholder, #ccc);
+		overflow: visible;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		color: #ccc;
+		/* #endif */
+
+		/* #ifdef MP-TOUTIAO */
+		padding-top: 0 !important;
+		/* #endif */
+	}
+
+	/* #ifdef MP */
+	::v-deep .tui-placeholder {
+		color: var(--thorui-text-color-placeholder, #ccc);
+		overflow: visible;
+	}
+
+	/* #endif */
+
+	.tui-textarea__border {
+		border-radius: 4rpx;
+		position: relative;
+		/* #ifdef APP-NVUE */
+		border-style: solid;
+		border-width: 0.5px;
+		border-color: #d1d1d1;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		border-width: 0;
+		/* #endif */
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-textarea__border::after {
+		content: ' ';
+		position: absolute;
+		height: 200%;
+		width: 200%;
+		border: 1px solid var(--thorui-border-color, #d1d1d1);
+		transform-origin: 0 0;
+		transform: scale(0.5);
+		left: 0;
+		top: 0;
+		border-radius: 8rpx;
+		pointer-events: none;
+	}
+
+	/* #endif */
+	.tui-textarea__flex-1 {
+		flex: 1;
+	}
+
+	.tui-textarea__counter {
+		padding-top: 8rpx;
+		text-align: right;
+		/* #ifdef APP-NVUE */
+		flex-direction: row;
+		flex: 1;
+		justify-content: flex-end;
+		/* #endif */
+	}
+
+	.tui-text__right {
+		text-align: right;
+	}
+</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>

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

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

@@ -0,0 +1,105 @@
+<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',
+	emits: ['close'],
+	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>

+ 122 - 0
components/thorui/tui-tr/tui-tr.vue

@@ -0,0 +1,122 @@
+<template>
+	<view
+		class="tui-table__row"
+		:class="{ 'tui-flex-wrap': flexWrap, 'tui-table__fixed': fixed }"
+		:style="{
+			backgroundColor: backgroundColor,
+			borderBottom: `${borderWidth} solid ${borderColor}`,
+			borderLeft: borderLeft ? `${borderWidth} solid ${borderColor}` : '0',
+			borderTop: borderTop ? `${borderWidth} solid ${borderColor}` : '0',
+			left: fixed ? left : 'auto',
+			right: fixed ? right : 'auto',
+			top: fixed ? top : 'auto',
+			marginTop: marginTop
+		}"
+		@tap="handleClick"
+	>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+//table tr
+export default {
+	name: 'tuiTr',
+	emits: ['click'],
+	options: {
+		// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现
+		virtualHost: true
+	},
+	props: {
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		},
+		//border-bottom width
+		borderWidth: {
+			type: String,
+			default: '1rpx'
+		},
+		//border-bottom color
+		borderColor: {
+			type: String,
+			default: '#EAEEF5'
+		},
+		borderLeft: {
+			type: Boolean,
+			default: false
+		},
+		borderTop: {
+			type: Boolean,
+			default: false
+		},
+		flexWrap: {
+			type: Boolean,
+			default: false
+		},
+		fixed: {
+			type: Boolean,
+			default: false
+		},
+		left: {
+			type: String,
+			default: '0'
+		},
+		right: {
+			type: String,
+			default: '0'
+		},
+		top: {
+			type: String,
+			// #ifdef H5
+			default: '44px',
+			// #endif
+			// #ifndef H5
+			default: '0'
+			// #endif
+		},
+		marginTop: {
+			type: String,
+			default: '0'
+		},
+		//行数索引
+		index: {
+			type: Number,
+			default: 0
+		},
+		//参数
+		params: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		handleClick() {
+			this.$emit('click', {
+				index: this.index,
+				params: this.params
+			});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-table__row {
+	width: 100%;
+	display: flex;
+	box-sizing: border-box;
+}
+
+.tui-flex-wrap {
+	flex-wrap: wrap;
+}
+
+.tui-table__fixed {
+	position: fixed;
+	z-index: 99;
+}
+</style>

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

@@ -0,0 +1,500 @@
+<template>
+	<view class="tui-upload__container">
+		<view class="tui-upload-box">
+			<view class="tui-image-item" :style="{width:width+'rpx',height:height+'rpx'}"
+				v-for="(item,index) in imageList" :key="index">
+				<image :src="item" class="tui-item-img" :style="{width:width+'rpx',height:height+'rpx'}"
+					@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" :style="{width:width+'rpx',height:height+'rpx'}"
+				@tap="chooseImage">
+				<view class="tui-upload-icon tui-icon-plus"></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiUpload',
+		emits: ['remove', 'complete'],
+		props: {
+			//展示图片宽度
+			width: {
+				type: [Number, String],
+				default: 220
+			},
+			//展示图片高度
+			height: {
+				type: [Number, String],
+				default: 220
+			},
+			//初始化图片路径
+			value: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			//删除图片前是否弹框确认
+			delConfirm: {
+				type: Boolean,
+				default: false
+			},
+			//禁用删除
+			forbidDel: {
+				type: Boolean,
+				default: false
+			},
+			//禁用添加
+			forbidAdd: {
+				type: Boolean,
+				default: false
+			},
+			//服务器接口地址。当接口地址为空时,直接返回本地图片地址
+			serverUrl: {
+				type: String,
+				default: ""
+			},
+			//限制数
+			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
+			},
+			//文件对应的key,默认为 file
+			fileKeyName: {
+				type: String,
+				default: "file"
+			},
+			//HTTP 请求 Header, header 中不能设置 Referer。
+			header: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			//HTTP 请求中其他额外的 form data
+			formData: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			//自定义参数
+			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.statusArr = [];
+				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()
+				})
+			},
+			/**
+			 * @param manual 是否手动上传
+			 **/
+			change(manual = false) {
+				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,
+					manual: manual
+				})
+			},
+			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
+							//服务器地址
+							if (_this.serverUrl) {
+								_this.uploadImage(index, imageArr[j]).then(() => {
+									_this.change()
+								}).catch(() => {
+									_this.change()
+								})
+							} else {
+								//无服务器地址则直接返回成功
+								_this.$set(_this.statusArr, index, "1")
+								_this.change()
+							}
+						}
+					}
+				})
+			},
+			uploadImage: function(index, url, serverUrl) {
+				let _this = this;
+				return new Promise((resolve, reject) => {
+					uni.uploadFile({
+						url: this.serverUrl || serverUrl,
+						name: this.fileKeyName,
+						header: this.header,
+						formData: this.formData,
+						filePath: url,
+						success: function(res) {
+							if (res.statusCode == 200) {
+								//返回结果 此处需要按接口实际返回进行修改
+								let d = JSON.parse(res.data.replace(/\ufeff/g, "") || "{}")
+								//判断code,以实际接口规范判断
+								if (d.code % 100 === 0) {
+									// 上传成功 d.url 为上传后图片地址,以实际接口返回为准
+									d.url && (_this.imageList[index] = d.url)
+									_this.$set(_this.statusArr, index, d.url ? "1" : "3")
+								} else {
+									// 上传失败
+									_this.$set(_this.statusArr, index, "3")
+								}
+								resolve(index)
+							} else {
+								_this.$set(_this.statusArr, index, "3")
+								reject(index)
+							}
+						},
+						fail: function(res) {
+							_this.$set(_this.statusArr, index, "3")
+							reject(index)
+						}
+					})
+				})
+
+			},
+			delImage: function(index) {
+				let that = this
+				if (this.delConfirm) {
+					uni.showModal({
+						title: '提示',
+						content: '确认删除该图片吗?',
+						showCancel: true,
+						cancelColor: "#555",
+						confirmColor: "#eb0909",
+						confirmText: "确定",
+						success(res) {
+							if (res.confirm) {
+								that.imageList.splice(index, 1)
+								that.statusArr.splice(index, 1)
+								that.$emit("remove", {
+									index: index,
+									params: that.params
+								})
+								that.change()
+							}
+						}
+					})
+				} else {
+					that.imageList.splice(index, 1)
+					that.statusArr.splice(index, 1)
+					that.$emit("remove", {
+						index: index,
+						params: that.params
+					})
+					that.change()
+				}
+			},
+			previewImage: function(index) {
+				if (!this.imageList.length) return;
+				uni.previewImage({
+					current: this.imageList[index],
+					loop: true,
+					urls: this.imageList
+				})
+			},
+			/**
+			 * 当属性serverUrl传空时,父级调用该方法一次性上传所有图片
+			 * @param serverUrl 服务器接口地址
+			 **/
+			uploadAllImage(serverUrl) {
+				if (!serverUrl) {
+					this.toast('服务器接口地址不能为空!');
+					return;
+				}
+				let imageArr = [...this.imageList]
+				const len = imageArr.length
+				for (let i = 0; i < len; i++) {
+					//如果是服务器地址图片则无需再次上传
+					if (imageArr[i].startsWith('https')) {
+						continue;
+					} else {
+						this.$set(this.statusArr, i, "2")
+						this.uploadImage(i, imageArr[i], serverUrl).then(() => {
+							if (i === len - 1) {
+								this.change(true)
+							}
+						}).catch(() => {
+							if (i === len - 1) {
+								this.change(true)
+							}
+						})
+					}
+				}
+			}
+		}
+	}
+</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 {
+		font-size: 68rpx;
+		font-weight: 100;
+		color: #888;
+		background-color: #F7F7F7;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 0;
+	}
+
+	.tui-image-item {
+		position: relative;
+		margin-right: 20rpx;
+		margin-bottom: 20rpx;
+	}
+
+	.tui-image-item:nth-of-type(3n) {
+		margin-right: 0;
+	}
+
+	.tui-item-img {
+		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: 200;
+	}
+
+	.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: center;
+		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: 1px solid #fff;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+		margin-top: 26rpx;
+	}
+
+	.tui-btn-hover {
+		opacity: 0.8;
+	}
+</style>

+ 225 - 0
components/thorui/tui-waterfall/tui-waterfall.vue

@@ -0,0 +1,225 @@
+<template>
+	<view class="tui-waterfall__box"
+		:style="{ paddingLeft: leftGap, paddingRight: rightGap, background: backgroundColor, borderRadius: radius }">
+		<view class="tui-waterfall__list" id="tui-waterfall__left" :style="{ marginRight: columnGap }">
+			<view v-for="(item, index) in leftList" :key="index">
+				<slot name="left" :entity="item" :index="index" :isList="columnCount==1"></slot>
+			</view>
+		</view>
+		<view v-if="columnCount > 1" class="tui-waterfall__list" id="tui-waterfall__right">
+			<view v-for="(item, index) in rightList" :key="index">
+				<slot name="right" :entity="item" :index="index" :isList="columnCount==1"></slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 列表中图片可以使用懒加载或者其他方式
+	 * 优先计算出图片高度,以达到最佳效果
+	 * App端建议使用weex的waterfall组件,使用案例详见:
+	 * [ThorUI组件库->pages->index->productNvue]
+	 */
+
+	export default {
+		name: 'tuiWaterfall',
+		// #ifdef MP-WEIXIN
+		options: {
+			multipleSlots: true
+		},
+		// #endif
+		props: {
+			//列表数据,不建议一次性加载过多数据
+			listData: {
+				type: Array,
+				default () {
+					return [];
+				}
+			},
+			//每页数据条数(固定条数),当总数据长度小于等于该数时表示第一页数据,刷新重置
+			pageSize: {
+				type: Number,
+				default: 10
+			},
+			//数据分组类型:1-简单左右分组,按顺序排列,伪瀑布流 2-计算左右容器高度进行分组
+			type: {
+				type: Number,
+				default: 1
+			},
+			//瀑布流列数,目前支持最大值为2
+			columnCount: {
+				type: Number,
+				default: 2
+			},
+			//列与列的间隙
+			columnGap: {
+				type: String,
+				default: '10rpx'
+			},
+			//左侧和列表的间隙
+			leftGap: {
+				type: String,
+				default: '0'
+			},
+			//右侧和列表的间隙
+			rightGap: {
+				type: String,
+				default: '0'
+			},
+			//列表背景色,可使用渐变色
+			backgroundColor: {
+				type: String,
+				default: 'transparent'
+			},
+			//列表外层容器圆角值
+			radius: {
+				type: String,
+				default: '0'
+			}
+		},
+		data() {
+			return {
+				leftListConst: [],
+				leftList: [],
+				rightList: [],
+				init: true
+			};
+		},
+		watch: {
+			listData(val) {
+				if (!this.init) {
+					this.columnChange();
+				}
+			},
+			columnCount(val) {
+				this.columnChange(val);
+			}
+		},
+		mounted() {
+			this.init = false;
+			this.columnChange();
+		},
+		methods: {
+			columnChange(val) {
+				if (this.columnCount < 2) {
+					this.leftList = [...this.listData];
+				} else {
+					if (val && val == 2) {
+						this.leftList = [...this.leftListConst]
+					}
+					this.initData()
+				}
+			},
+			initData() {
+				if (this.type == 1) {
+					this.getSubGroup();
+				} else {
+					this.getArrayByHeight();
+				}
+			},
+			getDiffList() {
+				let diffList = [];
+				let total = this.listData.length;
+				if (total <= this.pageSize) {
+					this.leftListConst = [];
+					this.leftList = [];
+					this.rightList = [];
+				}
+				let sum = this.leftListConst.length + this.rightList.length;
+				let diff = total - sum;
+				if (diff > 0) {
+					diffList = [...this.listData].filter((item, index) => {
+						return index >= sum;
+					});
+				}
+				return diffList;
+			},
+			getSubGroup() {
+				//type=1时执行简单数据分组
+				if (!this.listData && this.listData.length === 0) return;
+				let leftList = [];
+				let rightList = [];
+				let data = this.getDiffList();
+				data.forEach((item, index) => {
+					if (index % 2 === 0) {
+						leftList.push(item);
+					} else {
+						rightList.push(item);
+					}
+				});
+				this.leftList = this.leftList.concat(leftList);
+				this.leftListConst = this.leftListConst.concat(leftList);
+				this.rightList = this.rightList.concat(rightList);
+			},
+			async getArrayByHeight() {
+				if (!this.listData && this.listData.length === 0) return;
+				let data = this.getDiffList();
+				for (let item of data) {
+					await this.render(item);
+				}
+			},
+			sleep(millisecond) {
+				let now = new Date();
+				let exitTime = now.getTime() + millisecond;
+				while (true) {
+					now = new Date();
+					if (now.getTime() > exitTime) return;
+				}
+			},
+			async render(item) {
+				this.sleep(50)
+				let obj = await this.getContainerHeight();
+				return await new Promise((resolve, reject) => {
+					if (obj && typeof obj.leftHeight === 'number') {
+						if (obj.leftHeight <= obj.rightHeight) {
+							this.leftList.push(item);
+							this.leftListConst.push(item);
+						} else {
+							this.rightList.push(item);
+						}
+						resolve(true);
+					} else {
+						reject(false);
+					}
+				});
+			},
+			async getContainerHeight() {
+				//type=2
+				return await new Promise((resolve, reject) => {
+					const query = uni.createSelectorQuery().in(this);
+					let nodes = query.selectAll('#tui-waterfall__left, #tui-waterfall__right');
+					nodes.boundingClientRect().exec(res => {
+						if (res && res[0]) {
+							const rects = res[0];
+							const leftHeight = rects[0].height;
+							const rightHeight = rects[1].height;
+							resolve({
+								leftHeight: leftHeight,
+								rightHeight: rightHeight
+							});
+						} else {
+							reject(res);
+						}
+					});
+				});
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-waterfall__box {
+		width: 100%;
+		display: inline-flex;
+		justify-content: space-between;
+		flex-direction: row;
+		flex-wrap: wrap;
+		box-sizing: border-box;
+		align-items: flex-start;
+	}
+
+	.tui-waterfall__list {
+		flex: 1;
+	}
+</style>

+ 1 - 1
index.html

@@ -14,7 +14,7 @@
     <!--app-context-->
   </head>
   <body>
-    <div id="app"><!--app-html--></div>
+    <div id="app"></div>
     <script type="module" src="/main.js"></script>
   </body>
 </html>

+ 1 - 1
manifest.json

@@ -50,7 +50,7 @@
     "quickapp" : {},
     /* 小程序特有相关 */
     "mp-weixin" : {
-        "appid" : "",
+        "appid" : "wx2f5649d73f4e4f83",
         "setting" : {
             "urlCheck" : false,
             "es6" : true,

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