Browse Source

项目初始化

zhengjinyi 2 years ago
commit
3fb501fed5
100 changed files with 19410 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 30 0
      App.vue
  3. 45 0
      common/auth.js
  4. 113 0
      common/business.helper.js
  5. 96 0
      common/couponUtils.js
  6. 26 0
      common/crypto.js
  7. 137 0
      common/css/global.scss
  8. 6 0
      common/css/icon.css
  9. 65 0
      common/css/theme.scss
  10. 25 0
      common/filters.js
  11. 195 0
      common/goods.helper.js
  12. 0 0
      common/libs/crypto-js.min.js
  13. 0 0
      common/libs/picker.city.js
  14. 105 0
      common/share.helper.js
  15. 54 0
      common/storage.js
  16. 89 0
      common/uniapp.api.js
  17. 219 0
      common/utils.js
  18. 92 0
      common/validation.js
  19. 6 0
      components/common/tui-clipboard/clipboard.min.js
  20. 55 0
      components/common/tui-clipboard/tui-clipboard.js
  21. 0 0
      components/common/tui-color-analysis/tui-color-analysis.js
  22. 314 0
      components/common/tui-validation/tui-validation.js
  23. 0 0
      components/common/tui-validation/tui-validation.min.js
  24. 0 0
      components/common/tui-zh-pinyin/tui-zh-pinyin.js
  25. 187 0
      components/thorui/tui-actionsheet/tui-actionsheet.vue
  26. 135 0
      components/thorui/tui-alert/tui-alert.vue
  27. 156 0
      components/thorui/tui-badge/tui-badge.vue
  28. 386 0
      components/thorui/tui-bottom-navigation/tui-bottom-navigation.vue
  29. 107 0
      components/thorui/tui-bottom-popup/tui-bottom-popup.vue
  30. 204 0
      components/thorui/tui-bubble-popup/tui-bubble-popup.vue
  31. 554 0
      components/thorui/tui-button/tui-button.vue
  32. 562 0
      components/thorui/tui-calendar/tui-calendar.js
  33. 919 0
      components/thorui/tui-calendar/tui-calendar.vue
  34. 212 0
      components/thorui/tui-card/tui-card.vue
  35. 567 0
      components/thorui/tui-cascade-selection/tui-cascade-selection.vue
  36. 265 0
      components/thorui/tui-circular-progress/tui-circular-progress.vue
  37. 167 0
      components/thorui/tui-collapse/tui-collapse.vue
  38. 343 0
      components/thorui/tui-countdown/tui-countdown.vue
  39. 654 0
      components/thorui/tui-datetime/tui-datetime.vue
  40. 103 0
      components/thorui/tui-divider/tui-divider.vue
  41. 140 0
      components/thorui/tui-drawer/tui-drawer.vue
  42. 69 0
      components/thorui/tui-dropdown-list/tui-dropdown-list.vue
  43. 276 0
      components/thorui/tui-fab/tui-fab.vue
  44. 118 0
      components/thorui/tui-footer/tui-footer.vue
  45. 148 0
      components/thorui/tui-grid-item/tui-grid-item.vue
  46. 44 0
      components/thorui/tui-grid/tui-grid.vue
  47. 190 0
      components/thorui/tui-icon/tui-icon.js
  48. 13 0
      components/thorui/tui-icon/tui-icon.vue
  49. 1031 0
      components/thorui/tui-image-cropper/tui-image-cropper.vue
  50. 164 0
      components/thorui/tui-image-group/tui-image-group.vue
  51. 73 0
      components/thorui/tui-keyboard-input/tui-keyboard-input.vue
  52. 241 0
      components/thorui/tui-keyboard/tui-keyboard.vue
  53. 173 0
      components/thorui/tui-list-cell/tui-list-cell.vue
  54. 97 0
      components/thorui/tui-list-view/tui-list-view.vue
  55. 78 0
      components/thorui/tui-loading/tui-loading.vue
  56. 161 0
      components/thorui/tui-loadmore/tui-loadmore.vue
  57. 427 0
      components/thorui/tui-modal/tui-modal.vue
  58. 249 0
      components/thorui/tui-navigation-bar/tui-navigation-bar.vue
  59. 118 0
      components/thorui/tui-no-data/tui-no-data.vue
  60. 115 0
      components/thorui/tui-nomore/tui-nomore.vue
  61. 231 0
      components/thorui/tui-numberbox/tui-numberbox.vue
  62. 700 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.vue
  63. 560 0
      components/thorui/tui-picture-cropper/tui-picture-cropper.wxs
  64. 168 0
      components/thorui/tui-rate/tui-rate.vue
  65. 296 0
      components/thorui/tui-round-progress/tui-round-progress.vue
  66. 179 0
      components/thorui/tui-scroll-top/tui-scroll-top.vue
  67. 263 0
      components/thorui/tui-skeleton/tui-skeleton.vue
  68. 218 0
      components/thorui/tui-slide-verify/tui-slide-verify.vue
  69. 73 0
      components/thorui/tui-slide-verify/tui-slide-verify.wxs
  70. 255 0
      components/thorui/tui-steps/tui-steps.vue
  71. 125 0
      components/thorui/tui-sticky-wxs/tui-sticky-wxs.vue
  72. 44 0
      components/thorui/tui-sticky-wxs/tui-sticky.wxs
  73. 155 0
      components/thorui/tui-sticky/tui-sticky.vue
  74. 315 0
      components/thorui/tui-swipe-action/tui-swipe-action.vue
  75. 280 0
      components/thorui/tui-tabbar/tui-tabbar.vue
  76. 319 0
      components/thorui/tui-tabs/tui-tabs.vue
  77. 355 0
      components/thorui/tui-tag/tui-tag.vue
  78. 38 0
      components/thorui/tui-time-axis/tui-time-axis.vue
  79. 50 0
      components/thorui/tui-timeaxis-item/tui-timeaxis-item.vue
  80. 129 0
      components/thorui/tui-tips/tui-tips.vue
  81. 121 0
      components/thorui/tui-toast/tui-toast.vue
  82. 105 0
      components/thorui/tui-top-dropdown/tui-top-dropdown.vue
  83. 500 0
      components/thorui/tui-upload/tui-upload.vue
  84. 3 0
      components/uni/marked/index.js
  85. 1573 0
      components/uni/marked/lib/marked.js
  86. 18 0
      components/uni/uParse/src/components/wxParseAudio.vue
  87. 94 0
      components/uni/uParse/src/components/wxParseImg.vue
  88. 63 0
      components/uni/uParse/src/components/wxParseTable.vue
  89. 94 0
      components/uni/uParse/src/components/wxParseTemplate0.vue
  90. 88 0
      components/uni/uParse/src/components/wxParseTemplate1.vue
  91. 88 0
      components/uni/uParse/src/components/wxParseTemplate10.vue
  92. 86 0
      components/uni/uParse/src/components/wxParseTemplate11.vue
  93. 88 0
      components/uni/uParse/src/components/wxParseTemplate2.vue
  94. 88 0
      components/uni/uParse/src/components/wxParseTemplate3.vue
  95. 88 0
      components/uni/uParse/src/components/wxParseTemplate4.vue
  96. 88 0
      components/uni/uParse/src/components/wxParseTemplate5.vue
  97. 89 0
      components/uni/uParse/src/components/wxParseTemplate6.vue
  98. 88 0
      components/uni/uParse/src/components/wxParseTemplate7.vue
  99. 88 0
      components/uni/uParse/src/components/wxParseTemplate8.vue
  100. 88 0
      components/uni/uParse/src/components/wxParseTemplate9.vue

+ 1 - 0
.gitignore

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

+ 30 - 0
App.vue

@@ -0,0 +1,30 @@
+<script>
+export default {
+    onLaunch: function() {
+        const updateManager = uni.getUpdateManager()
+        updateManager.onCheckForUpdate(function(res) {
+            console.log(res.hasUpdate)
+        })
+        updateManager.onUpdateReady(function(res) {
+            uni.showModal({
+                title: '更新提示',
+                content: '新版本已经准备好,是否重启小程序?',
+                success(res) {
+                    if (res.confirm) {
+                        updateManager.applyUpdate()
+                    }
+                }
+            })
+        })
+        
+        this.$store.dispatch('app/initDevice')
+    }
+}
+</script>
+
+<style lang="scss">
+page {
+    min-height: 100%;
+    background: #f7f7f7;
+}
+</style>

+ 45 - 0
common/auth.js

@@ -0,0 +1,45 @@
+/* 用户授权相关 */
+
+// 获取微信code(用户登录)
+export function wxLogin() {
+    return new Promise((resolve, reject) => {
+        uni.login({
+            provider: 'weixin',
+            success(res) {
+                resolve(res.code)
+            },
+            fail() {
+                reject()
+            }
+        })
+    })
+}
+
+export function authorize(scope, callback) {
+    uni.authorize({
+        scope: `scope.${scope}`,
+        success() {
+            console.log(1)
+            callback()
+        },
+        fail(err) {
+            console.log(err)
+        }
+    })
+}
+
+
+// 获取用户微信信息
+export function getUserProfile() {
+    return new Promise((resolve, reject) => {
+        uni.getUserProfile({
+            desc: '获取头像、昵称', 
+            success(res) {
+                resolve(res.userInfo)
+            },
+            fail(err) {
+                reject(err)
+            }
+        })
+    })
+}

+ 113 - 0
common/business.helper.js

@@ -0,0 +1,113 @@
+// 获取已选商品列表
+export function totalAllCheckedProduct(shopList) {
+    let productList = []
+    shopList.forEach(shop => {
+        productList.push(...shop.productList.filter(product => product.checked))
+    })
+    return productList
+}
+
+// 计算购物车商品价格
+export function computeTotalPrice(productList) {
+    const { allPrice } = productList.reduce((prevPrice, product) => {
+        // 已选商品总价
+        prevPrice.originalPrice += product.price * product.num
+        // 单品满减
+        if (product.promotion && product.promotion.type == 1 && product.promotion.mode == 2) {
+            // 是否满足 满减条件
+            if (product.price * product.num >= product.promotion.touchPrice) {
+                prevPrice.reducedPrice += product.promotion.reducedPrice
+            }
+        }
+        prevPrice.allPrice = prevPrice.originalPrice - prevPrice.reducedPrice
+        return prevPrice
+    }, {
+        originalPrice: 0, // 商品总价
+        reducedPrice: 0, // 优惠金额
+        allPrice: 0 // 组后后价格
+    })
+    return allPrice
+}
+
+// 处理优惠券列表: 排序,优惠券点击类型
+export function initFormatCouponList(couponList = [], controlType, sort) {
+    couponList = couponList.map((coupon, index) => {
+        coupon.controlType = controlType
+        coupon.checked = false
+        coupon.uniqueId = index + 1
+        return coupon
+    })
+
+    if (sort) {
+        return couponList.sort((a, b) => b.couponAmount - a.couponAmount)
+    }
+
+    return couponList
+}
+
+// 将优惠券列表拆分为满足条件和为满足条件列表
+export function splitCouponList(couponList = [], productList = []) {
+    const result = {
+        canUseCouponList: [],
+        notUseCouponList: []
+    }
+    couponList.forEach(coupon => {
+        if (
+            coupon.noThresholdFlag === 1 ||
+            (coupon.productType === 1 && allProdoctUseCheck(productList, coupon)) ||
+            (coupon.productType === 2 && someProductUseCheck(productList, coupon))
+        ) {
+            result.canUseCouponList.push(coupon)
+        } else {
+            result.notUseCouponList.push(coupon)
+        }
+    })
+    return result
+}
+
+// 判断全部商品可用 (全部商品价格总计 是否大于 当前优惠券的触发金额)
+export function allProdoctUseCheck(productList, coupon) {
+    const countPrice = productList.reduce((countPrice, product) => countPrice + product.price * product.num, 0)
+    console.log('all', countPrice)
+    return countPrice >= coupon.touchPrice
+}
+
+// 判断指定商品可用 (当前优惠券可用的商品的价格总计 是否大于 当前优惠券的触发金额)
+export function someProductUseCheck(productList, coupon) {
+    const countPrice = productList.reduce((countPrice, product) => {
+        // 当前优惠券可用的商品总价
+        const isIncludes = coupon.productIds.indexOf(product.productId.toString()) > -1
+        return isIncludes ? countPrice + product.price * product.num : countPrice
+    }, 0)
+    console.log('some', countPrice)
+    return countPrice >= coupon.touchPrice
+}
+
+// 优惠券使用提示
+export function makeCouponUseTip(currentCoupon, nextCoupon, allPrice) {
+    // 两者都不存在
+    if (!currentCoupon && !nextCoupon) return ''
+
+    // 只有可使用券时
+    if (currentCoupon && !nextCoupon) {
+        if (currentCoupon.noThresholdFlag === 1) {
+            return `已享“减${currentCoupon.couponAmount}元”优惠券`
+        }
+
+        return `已享“满${currentCoupon.touchPrice}元减${currentCoupon.couponAmount}元”优惠券`
+    }
+
+    // 只有下阶段券时
+    if (!currentCoupon && nextCoupon) {
+        const { couponAmount: nextCouponAmount, touchPrice: nextTouchPrice } = nextCoupon
+        return `还差¥${(nextTouchPrice - allPrice).toFixed(2)}元可用“满${nextTouchPrice}元减${nextCouponAmount}元”优惠券`
+    }
+
+    // 全部同时有时
+    if (currentCoupon && nextCoupon) {
+        const { couponAmount: currentCouponAmount, touchPrice: currentTouchPrice } = currentCoupon
+        const { couponAmount: nextCouponAmount, touchPrice: nextTouchPrice } = nextCoupon
+        return `还差¥${(nextTouchPrice - allPrice).toFixed(2)}元可用“满${nextTouchPrice}元减${nextCouponAmount}元”优惠券`
+    }
+
+}

+ 96 - 0
common/couponUtils.js

@@ -0,0 +1,96 @@
+/**
+ * 取出优惠券列表中的最优优惠券和次优优惠券 
+ * */
+
+class CouponUtils {
+    constructor(couponList, productList) {
+        this.couponList = this.couponSort(this.getCanBeUseCouponList(couponList, productList))
+        this.productList = productList
+        this.bestCoupon = null
+        this.secondCoupon = null
+        this.currentIndex = this.couponList.length
+        this.fetchBestCoupon()
+    }
+
+    // 获取最高可用金额的优惠券
+    fetchBestCoupon() {
+        if (this.couponList.length === 0 || this.productList.length === 0) {
+            this.bestCoupon = null
+            return
+        }
+        this.bestCoupon = this.couponList.find(coupon => this.couponCanBeUse(coupon, this.productList)) || null
+        if (this.bestCoupon) {
+            this.currentIndex = this.couponList.findIndex(coupon => coupon.uniqueId === this.bestCoupon.uniqueId)
+        }
+        this.fetchSecondCoupon()
+    }
+
+    // 获取下一阶段可用优惠券
+    fetchSecondCoupon() {
+        if (this.couponList.length === 0 || this.productList.length === 0) {
+            this.secondCoupon = null
+            return
+        }
+        this.secondCoupon = this.currentIndex > 0 ? this.couponList[this.currentIndex - 1] : null
+    }
+
+    // 勾选商品满足优惠券使用条件 && 满足使用条件
+    couponCanBeUse(coupon, productList) {
+        return (
+            this.isNoThreshold(coupon) ||
+            this.isSomeProductUse(coupon, productList) ||
+            this.isAllProductUse(coupon, productList)
+        )
+    }
+
+    // 获取当前选中商品都能使用的优惠券
+    getCanBeUseCouponList(couponList, productList) {
+        return couponList.filter(coupon => coupon.productType === 1 || this.isCanUseByAllProduct(coupon,
+            productList))
+    }
+
+    // 全部商品可用
+    isCanUseByAllProduct(coupon, productList) {
+        return productList.every(product => coupon.productIds.indexOf(product.productId.toString()) > -1)
+    }
+
+    // 优惠券是否无门槛
+    isNoThreshold(coupon) {
+        return coupon.noThresholdFlag === 1
+    }
+
+    // 全部商品可用 && 满足使用条件
+    isAllProductUse(coupon, productList) {
+        if (coupon.productType !== 1) {
+            return false
+        }
+        const countPrice = productList.reduce((countPrice, product) => {
+            return countPrice + this.totalProductPrice(product)
+        }, 0)
+        return countPrice >= coupon.touchPrice
+    }
+
+    // 部分商品可用 && 满足使用条件
+    isSomeProductUse(coupon, productList) {
+        if (coupon.productType !== 2) {
+            return false
+        }
+        const countPrice = productList.reduce((countPrice, product) => {
+            const isIncludes = coupon.productIds.indexOf(product.productId.toString()) > -1
+            return isIncludes ? countPrice + this.totalProductPrice(product) : countPrice
+        }, 0)
+        return countPrice >= coupon.touchPrice
+    }
+
+    // 统计商品价格
+    totalProductPrice(product) {
+        return product.price * product.num
+    }
+
+    // 排序
+    couponSort(couponList) {
+        return couponList.sort((a, b) => b.couponAmount - a.couponAmount)
+    }
+}
+
+export default CouponUtils

+ 26 - 0
common/crypto.js

@@ -0,0 +1,26 @@
+import CryptoJS from './libs/crypto-js.min.js'
+
+const aseKey = 'HEHEMINI' //秘钥必须为:8/16/32位
+//加密
+export const encrypt = data => {
+    return CryptoJS.AES.encrypt(JSON.stringify(data), CryptoJS.enc.Utf8.parse(aseKey), {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7
+    }).toString()
+}
+//解密
+export const decrypt = encrypt => {
+    return CryptoJS.AES.decrypt(encrypt, CryptoJS.enc.Utf8.parse(aseKey), {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7
+    }).toString(CryptoJS.enc.Utf8)
+}
+
+const install = Vue => {
+    Vue.prototype.$crypto = {
+        encrypt,
+        decrypt
+    }
+}
+
+export default install

+ 137 - 0
common/css/global.scss

@@ -0,0 +1,137 @@
+/* 文字缩略 $line: 保留几行文章 */
+@mixin ellipsis($line: 1) {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: $line;
+    -webkit-box-orient: vertical;
+}
+
+// page {
+//     -webkit-filter: grayscale(100%);
+//     -moz-filter: grayscale(100%);
+//     -o-filter: grayscale(100%);
+//     filter: grayscale(100%);
+//     filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);
+// }
+// flex布局
+.cm-flex-center {
+    display: flex;
+    justify-content: center;
+
+    align-items: center;
+}
+.cm-flex-between {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.cm-flex-around {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+}
+// 标签
+.tags {
+    .tag {
+        display: inline-block;
+        margin-right: 8rpx;
+        height: 30rpx;
+        text-align: center;
+        line-height: 30rpx;
+        font-size: 18rpx;
+        padding: 0 4rpx;
+        vertical-align: middle;
+        &.cx {
+            // 促销 自营标签
+            background: #f83c6c;
+            border-radius: 4rpx;
+            color: #ffffff;
+        }
+        &.hd {
+            // 活动价标签
+            width: 80rpx;
+            box-sizing: border-box;
+            padding: 0;
+            background: url(https://static.caimei365.com/app/mini-hehe/icon/icon-active.png) top center no-repeat;
+            background-size: 80rpx 30rpx;
+            color: #f83c6c;
+        }
+        &.pt {
+            // 拼团价标签
+            background: linear-gradient(270deg, #ff457b 0%, #b03bb8 51%, #6431f2 100%);
+            color: #fff;
+            border-radius: 4rpx;
+        }
+        &.other {
+            // 其他标签
+            border: 1rpx solid #f83c6c;
+            height: 28rpx;
+            line-height: 28rpx;
+            color: #f83c6c;
+            border-radius: 4rpx;
+            max-width: 100%;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+}
+// 图标默认颜色
+.icon {
+    font-size: 36rpx;
+    color: #b2b2b2;
+    &.icon-xuanze {
+        color: #f83c6c;
+    }
+}
+// 列表为空的提示语样式
+.empty-tip {
+    font-size: 24rpx;
+    line-height: 1.6;
+    color: #999;
+}
+.placeholder {
+    font-size: 28rpx;
+    color: #999999;
+}
+.hover-class {
+    background-color: #eee !important;
+}
+// thorui 样式
+.tui-nodata-fixed {
+    transform: translate(-50%, -70%) !important;
+}
+.tui-divider {
+    .tui-divider-line {
+        background: #d4d4d4 !important;
+    }
+}
+// 定位
+.fixed-top {
+    position: fixed;
+    z-index: 90;
+    width: 100%;
+    left: 0;
+    top: 0;
+}
+.fixed-bottom {
+    position: fixed;
+    z-index: 90;
+    width: 100%;
+    left: 0;
+    bottom: 0;
+}
+.sticky-top {
+    position: sticky;
+    z-index: 90;
+    width: 100%;
+    left: 0;
+    top: 0;
+}
+// 清除浮动
+.clearfix::after {
+    content: '';
+    display: block;
+    clear: both;
+}

File diff suppressed because it is too large
+ 6 - 0
common/css/icon.css


+ 65 - 0
common/css/theme.scss

@@ -0,0 +1,65 @@
+// 产品相关
+$cm-price-color: #ff457b;
+$cm-name-color: #333333;
+$cm-color: #ff457b;
+
+/* 行为相关颜色 */
+$cm-color-primary: #007aff;
+$cm-color-success: #4cd964;
+$cm-color-warning: #f0ad4e;
+$cm-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$cm-text-color: #333; //基本色
+$cm-text-color-inverse: #fff; //反色
+$cm-text-color-grey: #999; //辅助灰色,如加载更多的提示信息
+$cm-text-color-placeholder: #808080;
+$cm-text-color-disable: #c0c0c0;
+
+/* 背景颜色 */
+$cm-bg-color: #ffffff;
+$cm-bg-color-grey: #f7f7f7;
+$cm-bg-color-hover: #f1f1f1; //点击状态颜色
+$cm-bg-color-mask: rgba(0, 0, 0, 0.4); //遮罩颜色
+
+/* 边框颜色 */
+$cm-border-color: #c8c7cc;
+$cm-border-color-active: #ff457b;
+
+/* 文字尺寸 */
+$cm-font-size-sm: 24rpx;
+$cm-font-size-base: 28rpx;
+$cm-font-size-lg: 32rpx;
+
+/* 图片尺寸 */
+$cm-img-size-sm: 40rpx;
+$cm-img-size-base: 52rpx;
+$cm-img-size-lg: 80rpx;
+
+/* Border Radius */
+$cm-border-radius-sm: 4rpx;
+$cm-border-radius-base: 6rpx;
+$cm-border-radius-lg: 12rpx;
+$cm-border-radius-circle: 50%;
+
+/* 水平间距 */
+$cm-spacing-row-sm: 8rpx;
+$cm-spacing-row-base: 16rpx;
+$cm-spacing-row-lg: 24rpx;
+
+/* 垂直间距 */
+$cm-spacing-col-sm: 8rpx;
+$cm-spacing-col-base: 16rpx;
+$cm-spacing-col-lg: 24rpx;
+
+/* 透明度 */
+$cm-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$cm-color-title: #2c405a; // 文章标题颜色
+$cm-font-size-title: 40rpx;
+$cm-color-subtitle: #555555; // 二级标题颜色
+$cm-font-size-subtitle: 52rpx;
+$cm-color-paragraph: #3f536e; // 文章段落颜色
+$cm-font-size-paragraph: 30rpx;
+

+ 25 - 0
common/filters.js

@@ -0,0 +1,25 @@
+import { dateFormat } from '@/common/utils.js'
+
+const install = Vue => {
+    // 格式化金额
+    Vue.filter('priceFormat', function(value) {
+        if (typeof value === 'undefined') return '未知'
+        if (typeof value !== 'number') {
+            value = parseFloat(value)
+        }
+        return value.toFixed(2) > 0 ? value.toFixed(2) : 0
+    })
+
+    // 格式化时间
+    Vue.filter('dateFormat', function(value) {
+        if (!value) return '未知'
+        if (value instanceof Date) {
+            return dateFormat(value, 'yyyy-MM-dd')
+        } else {
+            value = new Date(value)
+            return dateFormat(value, 'yyyy-MM-dd')
+        }
+    })
+}
+
+export default install

+ 195 - 0
common/goods.helper.js

@@ -0,0 +1,195 @@
+import { fetchProductDetail } from '@/services/api/goods.js'
+import { fetchCouponListByProductId } from '@/services/api/coupon.js'
+import store from '@/store/index.js'
+
+/* 获取产品活动类型 (拼团 活动价 限时特价) */
+export function generateActivityType(productData) {
+    const { collageStatus = 0, activeStatus = 0, discountStatus = 0 } = productData
+    // 拼团价
+    if (collageStatus > 0) {
+        return 'group' // 拼团价
+    }
+    // 限时活动
+    else if (discountStatus > 0) {
+        return 'time-limit'
+    }
+    // 普通活动价
+    else if (activeStatus > 0) {
+        return 'activity'
+    }
+    // 普通商品
+    return 'normal' // 普通价
+}
+
+/* 获取产品价格类型 */
+export function generatePriceType(productData) {
+    const { couponStatus = 0, collageStatus = 0, activeStatus = 0, discountStatus = 0, couponId } = productData
+    // 拼团价
+    if (collageStatus > 0) {
+        if (couponStatus === 1 && couponId) {
+            return 'groupWithCoupon' // 拼团券后价
+        } else {
+            return 'group' // 拼团价
+        }
+    }
+    // 限时活动
+    else if (discountStatus > 0 || activeStatus > 0) {
+        if (couponStatus === 1 && couponId) {
+            return 'activityWithCoupon' // 券后价
+        } else {
+            return 'normal' // 限时活动价格
+        }
+    }
+    // 无活动价
+    else {
+        if (couponStatus === 1 && couponId) {
+            return 'normalWithCoupon' // 普通券后价
+        } else {
+            return 'normal' // 普通价
+        }
+    }
+}
+
+/* 导航栏按钮类别 */
+const navbarButtonGroup = {
+    // 仅拼团
+    group: {
+        left: ['单独购买', '¥1000.00'],
+        right: ['拼团购买', '¥1000.00']
+    },
+
+    // 拼团 + 优惠券
+    groupWithCoupon: {
+        left: ['领券单独购买', '¥1000.00'],
+        right: ['领券拼团购买', '¥1000.00']
+    },
+
+    // 限时活动 / 活动价 + 优惠券
+    activityWithCoupon: {
+        left: ['加入购物车'],
+        right: ['领券购买', '¥1000.00']
+    },
+
+    // 限时活动 / 活动价 + 优惠券
+    normalWithCoupon: {
+        left: ['加入购物车'],
+        right: ['领券购买', '¥1000.00']
+    },
+
+    // 普通方式 不使用优惠券
+    normal: {
+        left: ['加入购物车'],
+        right: ['立即购买']
+    }
+}
+
+/* 导航按钮文本 */
+export function generateNavbarButtonText(productData) {
+    const { priceType } = productData
+    const navbarButton = navbarButtonGroup[priceType]
+    // 拼团券后价购买
+    if (priceType === 'groupWithCoupon') {
+        navbarButton.left[1] = `¥${productData.normalCouponPrice.toFixed(2)}`
+        navbarButton.right[1] = `¥${productData.couponPrice.toFixed(2)}`
+    }
+    // 拼团价购买
+    else if (priceType === 'group') {
+        navbarButton.left[1] = `¥${productData.normalPrice.toFixed(2)}`
+        navbarButton.right[1] = `¥${productData.price.toFixed(2)}`
+    }
+    // 活动价券后价购买(限时特价|普通活动)
+    else if (priceType === 'activityWithCoupon') {
+        navbarButton.right[1] = `¥${productData.couponPrice.toFixed(2)}`
+    }
+    // 普通价券后价购买
+    else if (priceType === 'normalWithCoupon') {
+        navbarButton.right[1] = `¥${productData.couponPrice.toFixed(2)}`
+    } else {
+        navbarButton.right[1] = ''
+    }
+    return navbarButton
+}
+
+/* 生成导航菜单类型 */
+export function generateNavbarType(productInfo) {
+    const { couponStatus = 0, collageStatus = 0, activeStatus = 0, discountStatus = 0 } = productInfo
+    // 拼团价
+    if (collageStatus > 0) {
+        if (couponStatus === 1) {
+            return 'groupWithCoupon' // 拼团券后价
+        } else {
+            return 'group' // 拼团价
+        }
+    }
+    // 限时活动
+    else if (discountStatus > 0 || activeStatus > 0) {
+        if (couponStatus === 1) {
+            return 'activityWithCoupon' // 券后价
+        } else {
+            return 'normal' // 限时活动价格
+        }
+    }
+    // 无活动价
+    else {
+        if (couponStatus === 1) {
+            return 'normalWithCoupon' // 普通券后价
+        } else {
+            return 'normal' // 普通价
+        }
+    }
+}
+
+/* 处理商品信息 */
+function generateProductInfo(product) {
+    if (!product) return product
+    // 商品活动类型
+    product.activityType = generateActivityType(product)
+    product.priceType = generatePriceType(product)
+    product.skus = product.skus.map(sku => {
+        sku.activityType = generateActivityType(sku)
+        sku.priceType = generatePriceType(sku)
+        return sku
+    })
+    return product
+}
+
+/* 创建优惠券 */
+function generateCoupon(coupon) {
+    const obj = Object.assign({}, coupon)
+    // 添加标题
+    if (coupon.noThresholdFlag > 0) {
+        obj.couponTitle = `减¥${coupon.couponAmount}元`
+    } else {
+        obj.couponTitle = `满¥${coupon.touchPrice}元减¥${coupon.couponAmount}元`
+    }
+    // 添加优惠券状态
+    if (obj.useStatus === 0) {
+        obj.controlType = 'receive'
+    } else if (obj.useStatus === 1) {
+        obj.couponStatus = 'received'
+        obj.controlType = 'search'
+    }
+    return obj
+}
+
+/* 获取商品详情 */
+export async function fetchPorductInfo(productId) {
+    try {
+        const userId = store.getters.userId
+        const res = await fetchProductDetail({ productId, userId })
+        return generateProductInfo(res.data)
+    } catch (e) {
+        console.log(e)
+    }
+}
+
+/* 获取商品可用优惠券 */
+export async function fetchCouponListByProduct(productId) {
+    try {
+        const userId = store.getters.userId
+        const res = await fetchCouponListByProductId({ productId, userId })
+        return res.data.map(coupon => generateCoupon(coupon))
+    } catch (e) {
+        console.log(e)
+    }
+}

File diff suppressed because it is too large
+ 0 - 0
common/libs/crypto-js.min.js


File diff suppressed because it is too large
+ 0 - 0
common/libs/picker.city.js


+ 105 - 0
common/share.helper.js

@@ -0,0 +1,105 @@
+import { encrypt } from '@/common/crypto.js'
+/* 小程序码 */
+import store from '@/store/index.js'
+import { wxUnlimited } from '@/services/api/auth.js'
+const fs = uni.getFileSystemManager()
+const qrcodePath = `${wx.env.USER_DATA_PATH}/qrcodePath`
+
+const defalutOptions = {
+    title: '护肤上颜选,正品有好货~',
+    path: '/pages/index/index',
+    imageUrl: 'https://static.caimei365.com/app/mini-hehe/icon/icon-share.png'
+}
+
+export function shareDataResult(shareData, title, coverUrl) {
+    const state_str = encodeURIComponent(encrypt(shareData))
+    const result = {
+        title: title || defalutOptions.title,
+        path: `${defalutOptions.path}?state_str=${state_str}`,
+        imageUrl: coverUrl || defalutOptions.imageUrl
+    }
+    return result
+}
+
+const queryKeyOfMap = {
+    'type': 't',
+    'inviteUserId': 'i',
+    'activityId': 'a',
+    'dealerUserId': 'd',
+    'keyWord': 'k',
+    'productId': 'p',
+    'jumpState': 'j',
+    'collageId': 'c'
+}
+
+const enQueryKeyOfMap = {
+    't': 'type',
+    'i': 'inviteUserId',
+    'a': 'activityId',
+    'd': 'dealerUserId',
+    'k': 'keyWord',
+    'p': 'productId',
+    'j': 'jumpState',
+    'c': 'collageId'
+}
+
+// 创建二维码保存路径
+function createQrcodeDir(callback) {
+    try {
+        fs.accessSync(qrcodePath)
+        console.log('已存在文件夹')
+        callback(qrcodePath)
+    } catch (e) {
+        fs.mkdirSync(qrcodePath)
+        console.log('不存在文件夹')
+        callback(qrcodePath)
+    }
+}
+
+
+/* 生成二维码链接 */
+export async function generateWxUnlimited(params) {
+    try {
+        // 从服务端获取二维码arrayBuffer
+        return await wxUnlimited({
+            page: params.pagePath || 'pages/index/index',
+            scene: codeQueryStr(params.queryStr),
+            check_path: process.env.NODE_ENV === 'production', // 是否校验页面
+            env_version: process.env.NODE_ENV === 'production' ? 'release' : 'trial', // 正式版 or 开发版
+            width: 200, // 二维码宽度
+            auto_color: false, // 自动颜色
+            line_color: { 'r': 0, 'g': 0, 'b': 0 }, // 线条颜色
+            is_hyaline: true // 透明底
+        })
+    } catch (e) {
+        return Promise.reject(e)
+    }
+}
+
+// 编码查询参数
+export function codeQueryStr(query = '') {
+    const keys = Object.keys(queryKeyOfMap)
+    return query.split('&').map(str => {
+        return str.split('=').map((substr, index) => {
+            if (!index) {
+                return queryKeyOfMap[keys.find(item => substr === item)]
+            } else {
+                return substr
+            }
+        }).join('=')
+    }).join('&')
+}
+
+// 反编码查询参数
+export function enCodeQueryStr(query) {
+    const keys = Object.keys(enQueryKeyOfMap)
+    return query.split('&').map(str => {
+        return str.split('=').map((substr, index) => {
+            if (!index) {
+                return enQueryKeyOfMap[keys.find(item => substr === item)]
+            } else {
+                return substr
+            }
+        }).join('=')
+    }).join('&')
+}

+ 54 - 0
common/storage.js

@@ -0,0 +1,54 @@
+import { encrypt, decrypt } from '@/common/crypto.js'
+
+// 在开发环境下不需要加密数据
+const encryptFlag = false
+
+// 预设字段头
+const prefix = 'HEHE_'
+
+// 缓存有效期 默认有效期为15天
+const expiredTime = 15 * 24 * 60 * 60 * 1000
+
+
+// 从缓存中读取数据(期限)
+export function setStorage(key, value, options = {}) {
+    if (!options.expiredTime) {
+        options.expiredTime = expiredTime
+    }
+    const nowTime = Date.now()
+    // 加密
+    if (encryptFlag) {
+        value = encrypt(value)
+    }
+    const payload = {
+        expiredTime: nowTime + options.expiredTime,
+        data: value
+    }
+    key = prefix + key
+    uni.setStorageSync(key, payload)
+}
+
+// 将数据添加到缓存中(期限)
+export function getStorage(key) {
+    key = prefix + key
+    const nowTime = Date.now()
+    const payload = uni.getStorageSync(key)
+    if (nowTime > payload.expiredTime) {
+        uni.removeStorageSync(key)
+        return null
+    }
+
+    // 解密
+    if (encryptFlag) {
+        payload.data = decrypt(payload.data)
+    }
+
+    return payload.data
+}
+
+const install = Vue => {
+    Vue.prototype.$setStorage = setStorage
+    Vue.prototype.$getStorage = getStorage
+}
+
+export default install

+ 89 - 0
common/uniapp.api.js

@@ -0,0 +1,89 @@
+export function toast(options) {
+    uni.showToast({
+        icon: options.icon || 'none',
+        mask: options.mask || true,
+        title: options.title || 'title'
+    })
+}
+toast.prototype.success = (message) => toast({ icon: 'success', title: message })
+toast.prototype.error = (message) => toast({ icon: 'error', title: message })
+
+const install = (Vue, options = {}) => {
+
+    const prefix = options.prefix || '/pages/views/'
+
+    /* tabbar 切换 */
+    const switchTab = (url, success, fail) => {
+        uni.switchTab({
+            url: `/pages/tabBar/${url}/${url}`,
+            success: success,
+            fail: fail
+        })
+    }
+
+    /* 保留当前页面跳转 */
+    const navigateTo = (url, success, fail) => {
+        uni.navigateTo({
+            url: `${prefix}${url}`,
+            success: success,
+            fail: fail
+        })
+    }
+
+    /* 关闭当前页面跳转 */
+    const redirectTo = (url, success, fail) => {
+        uni.redirectTo({
+            url: `${prefix}${url}`,
+            success: success,
+            fail: fail
+        })
+    }
+
+    /* 关闭所有页面跳转 */
+    const reLaunchTo = (url, success, fail) => {
+        uni.reLaunch({
+            url: `${prefix}${url}`,
+            success: success,
+            fail: fail
+        })
+    }
+
+    /* 关闭当前页面,返回上一页面或多级页面 */
+    const navigateBack = (delta, success, fail) => {
+        uni.navigateBack({
+            delta: delta || 1,
+            success: success,
+            fail: fail
+        })
+    }
+
+    /* 设置页面刷新标识 */
+    const addRefreshType = (mark = '') => {
+        uni.setStorageSync(`REFRESH_TYPE_${mark.toUpperCase()}`, true)
+    }
+
+    /* 获取页面刷新标识并判断是否需要刷新当前页面 */
+    const checkRefreshType = (mark = '') => {
+        const key = `REFRESH_TYPE_${mark.toUpperCase()}`
+        const type = uni.getStorageSync(key)
+        uni.removeStorageSync(key)
+        return type
+    }
+
+    Vue.prototype.$toast = (message) => toast({ title: message })
+    Vue.prototype.$toast.success = (message) => toast({ icon: 'success', title: message })
+    Vue.prototype.$toast.error = (message) => toast({ icon: 'error', title: message })
+
+    Vue.prototype.$router = {
+        switchTab,
+        navigateTo,
+        redirectTo,
+        reLaunchTo,
+        navigateBack,
+        addRefreshType,
+        checkRefreshType
+    }
+}
+
+
+export default install

+ 219 - 0
common/utils.js

@@ -0,0 +1,219 @@
+/**
+ * @description 防抖
+ * @param {Function} func 需要包装的函数
+ * @param {string} wait 等待执行时间
+ * @param {boolean} immediate 是否是立即执行 默认不立即执行
+ * @returns {Function} 返回包装后的函数
+ */
+export function debounce(func, wait, immediate = true) {
+    let timeout, result
+
+    return function() {
+        const context = this
+        const args = arguments
+        if (timeout) clearTimeout(timeout)
+        if (immediate) {
+            const callNow = !timeout
+            timeout = setTimeout(function() {
+                timeout = null
+            }, wait)
+            if (callNow) result = func.apply(context, args)
+        } else {
+            timeout = setTimeout(function() {
+                func.apply(context, args)
+            }, wait)
+        }
+        return result
+    }
+}
+
+
+/**
+ * @description 节流
+ * @param {Function} func 需要包装的函数
+ * @param {string} wait 间隔时间
+ * @returns {Function} 返回包装后的函数
+ */
+export function throttle(func, wait) {
+    let timeout
+    return function() {
+        const context = this
+        const args = arguments
+        if (!timeout) {
+            timeout = setTimeout(function() {
+                timeout = null
+                func.apply(context, args)
+            }, wait)
+        }
+    }
+}
+
+
+/**
+ * @description 深度克隆
+ * @param {object} obj 克隆目标
+ * @returns {object} 返回目标对象深度克隆后的结果
+ */
+export function deepClone(obj, cache = new WeakMap()) {
+    if (typeof obj !== 'object') return obj // 普通类型,直接返回
+    if (obj === null) return obj
+    if (cache.get(obj)) return cache.get(obj) // 防止循环引用,程序进入死循环
+    if (obj instanceof Date) return new Date(obj)
+    if (obj instanceof RegExp) return new RegExp(obj)
+
+    // 找到所属原型上的constructor,所属原型上的constructor指向当前对象的构造函数
+    let cloneObj = new obj.constructor()
+    cache.set(obj, cloneObj) // 缓存拷贝的对象,用于处理循环引用的情况
+    for (let key in obj) {
+        if (obj.hasOwnProperty(key)) {
+            cloneObj[key] = deepClone(obj[key], cache) // 递归拷贝
+        }
+    }
+    return cloneObj
+}
+
+
+/** 
+ * @description 时间日期格式化
+ * @param {dateTime} date 标准时间格式 -> new Date()
+ * @param {string} format 时间格式化的格式 'yyyy-MM-dd hh:mm:ss'
+ * @returns {string} 格式化后的时间  '2017-01-01 01:00:00'
+ */
+export function dateFormat(date = new Date(), format = 'yyyy-MM-dd hh:mm:ss') {
+    var o = {
+        'M+': date.getMonth() + 1, // month
+        'd+': date.getDate(), // day
+        'h+': date.getHours(), // hour
+        'm+': date.getMinutes(), // minute
+        's+': date.getSeconds(), // second
+        'q+': Math.floor((date.getMonth() + 3) / 3), // quarter
+        S: date.getMilliseconds(), // millisecond
+    }
+    if (/(y+)/.test(format)) {
+        format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+    }
+    for (var k in o) {
+        if (new RegExp('(' + k + ')').test(format)) {
+            format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k])
+                .length))
+        }
+    }
+    return format
+}
+
+
+/**
+ * @description 倒计时
+ * @param diff 倒计时时间(s)
+ * @param loadTime 运行时的当前时间(s)
+ * @param item 倒计时对象 默认:{ speed: 1000 }
+ * @param callback 回调
+ */
+export function countDown(diff, loadTime, item, callback) {
+    function round($diff) {
+        let dd = parseInt($diff / 1000 / 60 / 60 / 24, 10) // 计算剩余的天数
+        let hh = parseInt(($diff / 1000 / 60 / 60) % 24, 10) // 计算剩余的小时数
+        let mm = parseInt(($diff / 1000 / 60) % 60, 10) // 计算剩余的分钟数
+        let ss = parseInt(($diff / 1000) % 60, 10) // 计算剩余的秒数
+
+        function checkTime(_a) {
+            let a = _a
+            if (a < 10) {
+                a = '0' + a
+            }
+            return a.toString()
+        }
+
+        item.conttainer = {
+            ddhh: checkTime(dd * 24 + hh),
+            dd: checkTime(dd),
+            hh: checkTime(hh),
+            mm: checkTime(mm),
+            ss: checkTime(ss)
+        }
+
+        if (
+            item.conttainer.dd > 0 ||
+            item.conttainer.hh > 0 ||
+            item.conttainer.mm > 0 ||
+            item.conttainer.ss > 0
+        ) {
+            item.t = setTimeout(function() {
+                round($diff - (item.speed || 1000))
+            }, item.speed || 1000)
+        }
+        // 回调
+        callback && callback(item)
+    }
+    round(diff - loadTime)
+}
+
+
+/* 对象循环赋值 */
+export function objAssign(target = {}, source = {}) {
+    for (let key in target) {
+        if (source.hasOwnProperty(key)) {
+            target[key] = source[key]
+        }
+    }
+}
+
+/* 数组去重 */
+export function arrayUnique(arr) {
+    if (!Array.isArray(arr)) {
+        console.log('type error!')
+        return
+    }
+    return Array.from(new Set(arr))
+}
+
+
+/*
+ * 反序列化URL参数
+ * { age: "25", name: "Tom" }
+ */
+export function parseUrlSearch(location) {
+    return location.search
+        .replace(/(^\?)|(&$)/g, '')
+        .split('&')
+        .reduce((t, v) => {
+            const [key, val] = v.split('=')
+            t[key] = decodeURIComponent(val)
+            return t
+        }, {})
+}
+/*
+ * getQueryParams('id')
+ * 获取url上某个key的值
+ */
+export function getParam(param) {
+    // 获取浏览器参数
+    const r = new RegExp(`\\?(?:.+&)?${param}=(.*?)(?:&.*)?$`)
+    const m = window.location.toString().match(r)
+    return m ? decodeURI(m[1]) : ''
+}
+/*
+ * queryStringify
+ * 将k-v的对象序列化转成 url?k=v&k1=v1;
+ */
+export function queryStringify(search = {}) {
+    return Object.entries(search)
+        .reduce((t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`, '')
+        .replace(/&$/, '')
+}
+
+/*
+ * queryStringify
+ * 将url?k=v&k1=v1的序列化转成k-v对象
+ */
+export function queryParse(query = '') {
+    if (query.startsWith('?')) {
+        query = query.slice(0)
+    }
+    const obj = Object.create(null)
+    query.split('&').forEach(str => {
+        const v = str.split('=')
+        obj[v[0]] = v[1]
+    })
+    return obj
+}

+ 92 - 0
common/validation.js

@@ -0,0 +1,92 @@
+/**
+ * 是否是外部调用链接
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+	return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * 是否是URL链接
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+	const reg =
+		/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+	return reg.test(url)
+}
+
+/**
+ * 是否是小写字母
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+	const reg = /^[a-z]+$/
+	return reg.test(str)
+}
+
+/**
+ * 是否是大写字母
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+	const reg = /^[A-Z]+$/
+	return reg.test(str)
+}
+
+/**
+ * 是否是英文
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+	const reg = /^[A-Za-z]+$/
+	return reg.test(str)
+}
+
+/**
+ * 是否是邮箱
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+	const reg =
+		/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+	return reg.test(email)
+}
+
+/**
+ * 是否是手机号
+ * @param {string} mobile
+ * @returns {Boolean}
+ */
+export function validMobile(mobile) {
+	return /^1[23456789]\d{9}$/.test(mobile)
+}
+
+/**
+ * 是否是字符串
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+	if (typeof str === 'string' || str instanceof String) {
+		return true
+	}
+	return false
+}
+
+/**
+ * 是否是数组
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+	if (typeof Array.isArray === 'undefined') {
+		return Object.prototype.toString.call(arg) === '[object Array]'
+	}
+	return Array.isArray(arg)
+}

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


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

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

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


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

@@ -0,0 +1,314 @@
+/**
+ * 表单验证
+ * @author echo.
+ * @version 1.6.5
+ **/
+
+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);
+        return /^1[23456789]\d{9}$/.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
+}

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


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


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

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

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

@@ -0,0 +1,107 @@
+<template>
+	<view @touchmove.stop.prevent>
+		<view class="tui-popup-class tui-bottom-popup" :class="{ 'tui-popup-show': show, 'tui-popup-radius': radius }" :style="{ backgroundColor: backgroundColor, height: height ? height + 'rpx' : 'auto', zIndex: zIndex,transform:`translate3d(0, ${show?translateY:'100%'}, 0)`}">
+			<slot></slot>
+		</view>
+		<view class="tui-popup-mask" :class="[show ? 'tui-mask-show' : '']" :style="{ zIndex: maskZIndex }" v-if="mask" @tap="handleClose"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiBottomPopup',
+		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;
+	}
+
+	.tui-popup-radius {
+		border-top-left-radius: 24rpx;
+		border-top-right-radius: 24rpx;
+		padding-bottom: env(safe-area-inset-bottom);
+		overflow: hidden;
+	}
+
+	.tui-popup-show {
+		opacity: 1;
+		/* transform: translate3d(0, 0, 0); */
+	}
+
+	.tui-popup-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.6);
+		transition: all 0.3s ease-in-out;
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.tui-mask-show {
+		opacity: 1;
+		visibility: visible;
+	}
+</style>

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

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

@@ -0,0 +1,554 @@
+<template>
+	<button class="tui-btn" :class="[
+			plain ? 'tui-' + type + '-outline' : 'tui-btn-' + (type || 'primary'),
+			getDisabledClass(disabled, type, plain),
+			getShapeClass(shape, plain),
+			getShadowClass(type, shadow, plain),
+			bold ? 'tui-text-bold' : '',
+			link ? 'tui-btn__link' : ''
+		]" :hover-class="getHoverClass(disabled, type, plain)"
+		:style="{ width: width, height: height, lineHeight: height, fontSize: size + 'rpx', margin: margin }"
+		:loading="loading" :form-type="formType" :open-type="openType" @getuserinfo="bindgetuserinfo"
+		@getphonenumber="bindgetphonenumber" @contact="bindcontact" @error="binderror" :disabled="disabled"
+		@tap="handleClick">
+		<slot></slot>
+	</button>
+</template>
+
+<script>
+	export default {
+		name: 'tuiButton',
+		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-base {
+		background: linear-gradient(270deg, #f83c6c 0%, #fc32b4 100%) !important;
+		color: #fff;
+	}
+	
+	.tui-shadow-base {
+		box-shadow: 0 10rpx 14rpx 0 rgba(86, 119, 252, 0.2);
+	}
+    
+    .tui-base-hover {
+    	background: linear-gradient(270deg, #f8335e 0%, #fc2aa1 100%) !important;
+    	color: #e5e5e5 !important;
+    }
+    
+    .tui-base-outline::after {
+    	border: 1px solid #f83c6c !important;
+    }
+    
+    .tui-base-outline {
+    	color: #f83c6c !important;
+    	background: transparent;
+    }
+	
+	.tui-btn-primary {
+		background: #5677fc !important;
+		color: #fff;
+	}
+
+	.tui-shadow-primary {
+		box-shadow: 0 10rpx 14rpx 0 rgba(86, 119, 252, 0.2);
+	}
+
+	.tui-btn-danger {
+		background: #eb0909 !important;
+		color: #fff;
+	}
+
+	.tui-shadow-danger {
+		box-shadow: 0 10rpx 14rpx 0 rgba(235, 9, 9, 0.2);
+	}
+
+	.tui-btn-warning {
+		background: #fc872d !important;
+		color: #fff;
+	}
+
+	.tui-shadow-warning {
+		box-shadow: 0 10rpx 14rpx 0 rgba(252, 135, 45, 0.2);
+	}
+
+	.tui-btn-green {
+		background: #07c160 !important;
+		color: #fff;
+	}
+
+	.tui-shadow-green {
+		box-shadow: 0 10rpx 14rpx 0 rgba(7, 193, 96, 0.2);
+	}
+
+	.tui-btn-blue {
+		background: #007aff !important;
+		color: #fff;
+	}
+
+	.tui-shadow-blue {
+		box-shadow: 0 10rpx 14rpx 0 rgba(0, 122, 255, 0.2);
+	}
+
+	.tui-btn-white {
+		background: #fff !important;
+		color: #333 !important;
+	}
+
+	.tui-btn-gray {
+		background: #bfbfbf !important;
+		color: #fff !important;
+	}
+
+	.tui-btn-black {
+		background: #333 !important;
+		color: #fff !important;
+	}
+
+	.tui-btn-brown {
+		background: #ac9157 !important;
+		color: #fff !important;
+	}
+
+	.tui-btn-gray-black {
+		background: #f2f2f2 !important;
+		color: #333;
+	}
+
+	.tui-btn-gray-primary {
+		background: #f2f2f2 !important;
+		color: #5677fc !important;
+	}
+
+	.tui-gray-primary-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-green {
+		background: #f2f2f2 !important;
+		color: #07c160 !important;
+	}
+
+	.tui-gray-green-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-danger {
+		background: #f2f2f2 !important;
+		color: #eb0909 !important;
+	}
+
+	.tui-gray-danger-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-btn-gray-warning {
+		background: #f2f2f2 !important;
+		color: #fc872d !important;
+	}
+
+	.tui-gray-warning-hover {
+		background: #d9d9d9 !important;
+	}
+
+	.tui-shadow-gray {
+		box-shadow: 0 10rpx 14rpx 0 rgba(191, 191, 191, 0.2);
+	}
+
+	.tui-hover-gray {
+		background: #f7f7f9 !important;
+	}
+
+	.tui-black-hover {
+		background: #555 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-brown-hover {
+		background: #A37F49 !important;
+		color: #e5e5e5 !important;
+	}
+
+	/* button start*/
+
+	.tui-btn {
+		width: 100%;
+		position: relative;
+		border: 0 !important;
+		border-radius: 6rpx;
+		padding-left: 0;
+		padding-right: 0;
+		overflow: visible;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-btn::after {
+		content: '';
+		position: absolute;
+		width: 200%;
+		height: 200%;
+		transform-origin: 0 0;
+		transform: scale(0.5, 0.5) translateZ(0);
+		box-sizing: border-box;
+		left: 0;
+		top: 0;
+		border-radius: 12rpx;
+		border: 0;
+	}
+
+	.tui-text-bold {
+		font-weight: bold;
+	}
+
+	.tui-btn-white::after {
+		border: 1px solid #bfbfbf;
+	}
+
+	.tui-white-hover {
+		background: #e5e5e5 !important;
+		color: #2e2e2e !important;
+	}
+
+	.tui-dark-disabled {
+		opacity: 0.6 !important;
+		color: #fafbfc !important;
+	}
+
+	.tui-dark-disabled-outline {
+		opacity: 0.5 !important;
+	}
+
+	.tui-gray-disabled {
+		background: #f3f3f3 !important;
+		color: #919191 !important;
+		box-shadow: none;
+	}
+
+	.tui-outline-hover {
+		opacity: 0.5;
+	}
+	
+	
+	.tui-primary-hover {
+		background: #4a67d6 !important;
+		color: #e5e5e5 !important;
+	}
+    
+    
+
+	.tui-primary-outline::after {
+		border: 1px solid #5677fc !important;
+	}
+
+	.tui-primary-outline {
+		color: #5677fc !important;
+		background: transparent;
+	}
+
+	.tui-danger-hover {
+		background: #c80808 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-danger-outline {
+		color: #eb0909 !important;
+		background: transparent;
+	}
+
+	.tui-danger-outline::after {
+		border: 1px solid #eb0909 !important;
+	}
+
+	.tui-warning-hover {
+		background: #d67326 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-warning-outline {
+		color: #fc872d !important;
+		background: transparent;
+	}
+
+	.tui-warning-outline::after {
+		border: 1px solid #fc872d !important;
+	}
+
+	.tui-green-hover {
+		background: #06ad56 !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-green-outline {
+		color: #07c160 !important;
+		background: transparent;
+	}
+
+	.tui-green-outline::after {
+		border: 1px solid #07c160 !important;
+	}
+
+	.tui-blue-hover {
+		background: #0062cc !important;
+		color: #e5e5e5 !important;
+	}
+
+	.tui-blue-outline {
+		color: #007aff !important;
+		background: transparent;
+	}
+
+	.tui-blue-outline::after {
+		border: 1px solid #007aff !important;
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-btn-gradual {
+		background: linear-gradient(90deg, rgb(255, 89, 38), rgb(240, 14, 44)) !important;
+		color: #fff !important;
+	}
+
+	.tui-shadow-gradual {
+		box-shadow: 0 10rpx 14rpx 0 rgba(235, 9, 9, 0.15);
+	}
+
+	/* #endif */
+
+	.tui-gray-hover {
+		background: #a3a3a3 !important;
+		color: #898989;
+	}
+
+	/* #ifndef APP-NVUE */
+	.tui-gradual-hover {
+		background: linear-gradient(90deg, #d74620, #cd1225) !important;
+		color: #fff !important;
+	}
+
+	/* #endif */
+
+	.tui-gray-outline {
+		color: #999 !important;
+		background: transparent !important;
+	}
+
+	.tui-white-outline {
+		color: #fff !important;
+		background: transparent !important;
+	}
+
+	.tui-black-outline {
+		background: transparent !important;
+		color: #333 !important;
+	}
+
+	.tui-gray-outline::after {
+		border: 1px solid #ccc !important;
+	}
+
+	.tui-white-outline::after {
+		border: 1px solid #fff !important;
+	}
+
+	.tui-black-outline::after {
+		border: 1px solid #333 !important;
+	}
+
+	.tui-brown-outline {
+		color: #ac9157 !important;
+		background: transparent;
+	}
+
+	.tui-brown-outline::after {
+		border: 1px solid #ac9157 !important;
+	}
+
+	/*圆角 */
+
+	.tui-fillet {
+		border-radius: 50rpx;
+	}
+
+	.tui-btn-white.tui-fillet::after {
+		border-radius: 98rpx;
+	}
+
+	.tui-outline-fillet::after {
+		border-radius: 98rpx;
+	}
+
+	/*平角*/
+	.tui-rightAngle {
+		border-radius: 0;
+	}
+
+	.tui-btn-white.tui-rightAngle::after {
+		border-radius: 0;
+	}
+
+	.tui-outline-rightAngle::after {
+		border-radius: 0;
+	}
+
+	.tui-btn__link::after {
+		border: 0 !important;
+	}
+</style>

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

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

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

@@ -0,0 +1,919 @@
+<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"
+	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);
+				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>

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

@@ -0,0 +1,212 @@
+<template>
+	<view class="tui-card-class tui-card" :class="[full?'tui-card-full':'',border?'tui-card-border':'']" @tap="handleClick"
+	 @longtap="longTap">
+		<view class="tui-card-header" :class="{'tui-header-line':header.line}" :style="{background:header.bgcolor || '#fff'}">
+			<view class="tui-header-left">
+				<image :src="image.url" class="tui-header-thumb" :class="{'tui-thumb-circle':image.circle}" mode="widthFix" v-if="image.url"
+				 :style="{height:(image.height || 60)+'rpx',width:(image.width || 60)+'rpx'}"></image>
+				<text class="tui-header-title" :style="{fontSize:(title.size || 30)+'rpx',color:(title.color || '#7A7A7A')}" v-if="title.text">{{title.text}}</text>
+			</view>
+			<view class="tui-header-right" :style="{fontSize:(tag.size || 24)+'rpx',color:(tag.color || '#b2b2b2')}" v-if="tag.text">
+				{{tag.text}}
+			</view>
+		</view>
+		<view class="tui-card-body">
+			<slot name="body"></slot>
+		</view>
+		<view class="tui-card-footer">
+			<slot name="footer"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiCard",
+		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>

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

@@ -0,0 +1,567 @@
+<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="currentTab" duration="300" @change="switchTab"
+			:style="{ height: height, backgroundColor: backgroundColor }">
+			<swiper-item v-for="(item, index) in selectedArr" :key="index">
+				<scroll-view scroll-y :scroll-into-view="item.scrollViewId" class="tui-selection-item"
+					:style="{ height: height }">
+					<view class="tui-first-item" :style="{ height: firstItemTop }"></view>
+					<view class="tui-selection-cell" :style="{ padding: padding, backgroundColor: backgroundColor }"
+						:id="`id_${subIndex}`" v-for="(subItem, subIndex) in item.list" :key="subIndex"
+						@tap="change(index, subIndex, subItem)">
+						<icon type="success_no_circle" v-if="item.index === subIndex" :color="checkMarkColor"
+							:size="checkMarkSize" class="tui-icon-success"></icon>
+						<image :src="subItem.src" v-if="subItem.src" class="tui-cell-img"
+							:style="{ width: imgWidth, height: imgHeight, borderRadius: radius }"></image>
+						<view class="tui-cell-title"
+							:class="{ 'tui-font-bold': item.index === subIndex && textBold, 'tui-flex-shrink': nowrap }"
+							:style="{ color: item.index === subIndex ? textActiveColor : textColor, fontSize: textSize + 'rpx' }">
+							{{ subItem.text }}
+						</view>
+						<view class="tui-cell-sub_title" :style="{ color: subTextColor, fontSize: subTextSize + 'rpx' }"
+							v-if="subItem.subText">{{ subItem.subText }}</view>
+					</view>
+				</scroll-view>
+			</swiper-item>
+		</swiper>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'tuiCascadeSelection',
+		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,
+				value: []
+			},
+			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,
+				//tab栏scrollview滚动的位置
+				scrollViewId: 'id__1',
+				selectedArr: []
+			};
+		},
+		methods: {
+			setDefaultData(val) {
+				let defaultItemList = 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.currentTab = selectedArr.length - 1;
+						this.$nextTick(() => {
+							this.checkCor();
+						});
+					} else {
+						defaultItemList.map(item => {
+							item.scrollViewId = `id_${item.index}`;
+						});
+						this.selectedArr = defaultItemList;
+						this.currentTab = defaultItemList.length - 1;
+						this.$nextTick(() => {
+							this.checkCor();
+						});
+					}
+
+				} else {
+					this.initData(this.itemList, -1);
+				}
+			},
+			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 == -1 ? index : value;
+					list = arr[value].children || [];
+					if (layer > 0) {
+						for (let i = 1; i < layer + 1; i++) {
+							let val = layer === i ? index : selectedArr[i].index;
+							list = list[val].children || [];
+							if (list.length === 0) break;
+						}
+					}
+					list = this.removeChildren(list);
+				}
+				return list;
+			},
+			//滚动切换
+			switchTab: function(e) {
+				this.currentTab = e.detail.current;
+				this.checkCor();
+			},
+			//点击标题切换当
+			swichNav: function(e) {
+				let cur = e.currentTarget.dataset.current;
+				if (this.currentTab != cur) {
+					this.currentTab = cur;
+				}
+			},
+			checkCor: function() {
+				let item = this.selectedArr[this.currentTab];
+				item.scrollViewId = 'id__1';
+				this.$nextTick(() => {
+					setTimeout(() => {
+						let val = item.index < 2 ? 0 : Number(item.index - 2);
+						item.scrollViewId = `id_${val}`;
+					}, 2);
+				});
+
+				if (this.currentTab > 1) {
+					this.scrollViewId = `id_${this.currentTab - 1}`;
+				} else {
+					this.scrollViewId = `id_0`;
+				}
+			},
+			change(index, subIndex, subItem) {
+				let item = this.selectedArr[index];
+				if (item.index == subIndex) return;
+				item.index = subIndex;
+				item.text = subItem.text;
+				item.value = subItem.value;
+				item.subText = subItem.subText || '';
+				item.src = subItem.src || '';
+				this.$emit('change', {
+					layer: index,
+					subIndex: subIndex, //layer=> Array index
+					...subItem
+				});
+
+				if (!this.request) {
+					let data = this.getItemList(index, subIndex);
+					this.subLevelData(data, index);
+				}
+			},
+			//新增子级数据时处理
+			subLevelData(data, layer) {
+				if (!data || data.length === 0) {
+					if (layer == -1) return;
+					//完成选择
+					let 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);
+					}
+					this.$nextTick(() => {
+						this.currentTab = this.selectedArr.length - 1;
+					});
+				}
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-cascade-selection {
+		width: 100%;
+		box-sizing: border-box;
+	}
+
+	.tui-selection-header {
+		width: 100%;
+		display: flex;
+		align-items: center;
+		position: relative;
+		box-sizing: border-box;
+	}
+
+	.tui-bottom-line {
+		position: relative;
+	}
+
+	.tui-bottom-line::after {
+		width: 100%;
+		content: '';
+		position: absolute;
+		border-bottom: 1rpx solid #eaeef1;
+		-webkit-transform: scaleY(0.5) translateZ(0);
+		transform: scaleY(0.5) translateZ(0);
+		transform-origin: 0 100%;
+		bottom: 0;
+		right: 0;
+		left: 0;
+	}
+
+	.tui-btm-none::after {
+		border-bottom: 0 !important;
+	}
+
+	.tui-header-item {
+		max-width: 240rpx;
+		padding: 15rpx 30rpx;
+		box-sizing: border-box;
+		flex-shrink: 0;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		position: relative;
+	}
+
+	.tui-font-bold {
+		font-weight: bold;
+	}
+
+	.tui-active-line {
+		width: 60rpx;
+		height: 6rpx;
+		border-radius: 4rpx;
+		position: absolute;
+		bottom: 0;
+		right: 0;
+		left: 50%;
+		transform: translateX(-50%);
+	}
+
+	.tui-selection-cell {
+		width: 100%;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+	}
+
+	.tui-icon-success {
+		margin-right: 12rpx;
+	}
+
+	.tui-cell-img {
+		margin-right: 12rpx;
+		flex-shrink: 0;
+	}
+
+	.tui-cell-title {
+		word-break: break-all;
+	}
+
+	.tui-flex-shrink {
+		flex-shrink: 0;
+	}
+
+	.tui-font-bold {
+		font-weight: bold;
+	}
+
+	.tui-cell-sub_title {
+		margin-left: 20rpx;
+		word-break: break-all;
+	}
+
+	.tui-first-item {
+		width: 100%;
+	}
+</style>

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

@@ -0,0 +1,265 @@
+<template>
+	<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
+		<canvas
+			class="tui-circular-default"
+			:canvas-id="defaultCanvasId"
+			:id="defaultCanvasId"
+			:style="{ width: diam + 'px', height: (height || diam) + 'px' }"
+			v-if="defaultShow"
+		></canvas>
+		<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId" :style="{ width: diam + 'px', height: (height || diam) + 'px' }"></canvas>
+		<slot />
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tuiCircularProgress',
+	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: ''
+		},
+		//起始弧度,单位弧度
+		sAngle: {
+			type: Number,
+			default: -Math.PI / 2
+		},
+		//指定弧度的方向是逆时针还是顺时针。默认是false,即顺时针
+		counterclockwise: {
+			type: Boolean,
+			default: false
+		},
+		//进度百分比 [10% 传值 10]
+		percentage: {
+			type: Number,
+			default: 0
+		},
+		//进度百分比缩放倍数[使用半弧为100%时,则可传2]
+		multiple: {
+			type: Number,
+			default: 1
+		},
+		//动画执行时间[单位毫秒,低于50无动画]
+		duration: {
+			type: Number,
+			default: 800
+		},
+		//backwards: 动画从头播;forwards:动画从上次结束点接着播
+		activeMode: {
+			type: String,
+			default: 'backwards'
+		}
+	},
+	watch: {
+		percentage(val) {
+			this.initDraw();
+		}
+	},
+	data() {
+		return {
+			// #ifdef MP-WEIXIN
+			progressCanvasId: 'progressCanvasId',
+			defaultCanvasId: 'defaultCanvasId',
+			// #endif
+			// #ifndef MP-WEIXIN
+			progressCanvasId: this.getCanvasId(),
+			defaultCanvasId: this.getCanvasId(),
+			// #endif
+			progressContext: null,
+			linearGradient: null,
+			//起始百分比
+			startPercentage: 0
+			// dpi
+			//pixelRatio: uni.getSystemInfoSync().pixelRatio
+		};
+	},
+	mounted() {
+		this.initDraw(true);
+	},
+	methods: {
+		//初始化绘制
+		initDraw(init) {
+			let start = this.activeMode === 'backwards' ? 0 : this.startPercentage;
+			start = start > this.percentage ? 0 : start;
+			if (this.defaultShow && init) {
+				this.drawDefaultCircular();
+			}
+			this.drawProgressCircular(start);
+		},
+		//默认(背景)圆环
+		drawDefaultCircular() {
+			let ctx = uni.createCanvasContext(this.defaultCanvasId, this);
+			ctx.setLineWidth(this.lineWidth);
+			ctx.setStrokeStyle(this.defaultColor);
+			//终止弧度
+			let eAngle = Math.PI * (this.height ? 1 : 2) + this.sAngle;
+			this.drawArc(ctx, eAngle);
+		},
+		//进度圆环
+		drawProgressCircular(startPercentage) {
+			let ctx = this.progressContext;
+			let gradient = this.linearGradient;
+			if (!ctx) {
+				ctx = uni.createCanvasContext(this.progressCanvasId, this);
+				//创建一个线性的渐变颜色 CanvasGradient对象
+				gradient = ctx.createLinearGradient(0, 0, this.diam, 0);
+				gradient.addColorStop('0', this.progressColor);
+				if (this.gradualColor) {
+					gradient.addColorStop('1', this.gradualColor);
+				}
+				// #ifdef APP-PLUS || MP
+				const res = uni.getSystemInfoSync();
+				if (!this.gradualColor && res.platform.toLocaleLowerCase() == 'android') {
+					gradient.addColorStop('1', this.progressColor);
+				}
+				// #endif
+				this.progressContext = ctx;
+				this.linearGradient = gradient;
+			}
+			ctx.setLineWidth(this.lineWidth);
+			ctx.setStrokeStyle(gradient);
+			let time = this.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) {
+				ctx.setFontSize(this.fontSize);
+				ctx.setFillStyle(this.fontColor);
+				ctx.setTextAlign('center');
+				ctx.setTextBaseline('middle');
+				let percentage = this.percentText;
+				if (!percentage) {
+					percentage = this.counterclockwise ? 100 - startPercentage * this.multiple : startPercentage * this.multiple;
+					percentage = `${percentage}%`;
+				}
+				let radius = this.diam / 2;
+				ctx.fillText(percentage, radius, radius);
+			}
+			if (this.percentage == 0 || (this.counterclockwise && startPercentage == 100)) {
+				ctx.draw();		
+			}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
+			ctx.arc(radius, radius, radius - this.lineWidth, this.sAngle, eAngle, this.counterclockwise);
+			ctx.stroke();
+			ctx.draw();
+		},
+		//生成canvasId
+		getCanvasId() {
+			let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+				return (c === 'x' ? (Math.random() * 16) | 0 : 'r&0x3' | '0x8').toString(16);
+			});
+			return uuid;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-circular-container,
+.tui-circular-default {
+	position: relative;
+}
+
+.tui-circular-progress {
+	position: absolute;
+	left: 0;
+	top: 0;
+	z-index: 10;
+}
+</style>

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

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

@@ -0,0 +1,343 @@
+<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,
+			default: 0
+		},
+		//是否包含天
+		days: {
+			type: Boolean,
+			default: false
+		},
+		//是否包含小时
+		hours: {
+			type: Boolean,
+			default: true
+		},
+		//是否包含分钟
+		minutes: {
+			type: Boolean,
+			default: true
+		},
+		//是否包含秒
+		seconds: {
+			type: Boolean,
+			default: true
+		},
+		//单位用英文缩写表示 仅seconds秒数有效
+		unitEn: {
+			type: Boolean,
+			default: false
+		},
+		//是否展示为冒号,false为文字
+		isColon: {
+			type: Boolean,
+			default: true
+		},
+		//是否返回剩余时间
+		returnTime: {
+			type: Boolean,
+			default: false
+		},
+		//是否显示毫秒
+		isMs: {
+			type: Boolean,
+			default: false
+		},
+		msWidth: {
+			type: Number,
+			default: 32
+		},
+		msSize: {
+			type: Number,
+			default: 24
+		},
+		msColor: {
+			type: String,
+			default: '#333'
+		}
+	},
+	watch: {
+		time(val) {
+			this.clearTimer();
+			this.doLoop();
+		}
+	},
+	data() {
+		return {
+			countdown: null,
+			d: '0',
+			h: '00',
+			i: '00',
+			s: '00',
+			//此处若从9到1,结束需要特殊处理
+			ms: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+			ani: false
+		};
+	},
+	created() {
+		this.clearTimer();
+		this.doLoop();
+	},
+	// #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() {
+			this.ani = false;
+			this.clearTimer();
+			this.$emit('end', {});
+		},
+		doLoop: function() {
+			let seconds = this.time || 0;
+			this.ani = true;
+			this.countDown(seconds);
+			this.countdown = setInterval(() => {
+				seconds--;
+				if (seconds < 0) {
+					this.endOfTime();
+					return;
+				}
+				this.countDown(seconds);
+				if (this.returnTime) {
+					this.$emit('time', { seconds: seconds });
+				}
+			}, 1000);
+		},
+		countDown(seconds) {
+			let [day, hour, minute, second] = [0, 0, 0, 0];
+			if (seconds > 0) {
+				day = this.days ? Math.floor(seconds / (60 * 60 * 24)) : 0;
+				hour = this.hours ? Math.floor(seconds / (60 * 60)) - day * 24 : 0;
+				minute = this.minutes ? Math.floor(seconds / 60) - hour * 60 - day * 24 * 60 : 0;
+				second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
+			} else {
+				this.endOfTime();
+			}
+			hour = hour < 10 ? '0' + hour : hour;
+			minute = minute < 10 ? '0' + minute : minute;
+			second = second < 10 ? '0' + second : second;
+			this.d = day;
+			this.h = hour;
+			this.i = minute;
+			this.s = second;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-countdown-box {
+	display: flex;
+	align-items: center;
+}
+
+.tui-countdown-box {
+	display: flex;
+	align-items: center;
+}
+
+.tui-countdown-item {
+	border: 1rpx solid;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 6rpx;
+	white-space: nowrap;
+	transform: translateZ(0);
+}
+
+.tui-countdown-time {
+	margin: 0;
+	padding: 0;
+}
+
+.tui-countdown-colon {
+	display: flex;
+	justify-content: center;
+	padding: 0 5rpx;
+}
+
+.tui-colon-pad {
+	padding: 0 !important;
+}
+
+.tui-countdown-scale {
+	transform: scale(0.9);
+	transform-origin: center center;
+}
+.tui-countdown__ms {
+	border: 1rpx solid;
+	overflow: hidden;
+	border-radius: 6rpx;
+}
+
+/*ms使用css3代替js频繁更新操作,性能优化*/
+.tui-ms__list {
+	animation: loop 1s steps(10) infinite;
+}
+
+@keyframes loop {
+	from {
+		transform: translateY(0);
+	}
+
+	to {
+		transform: translateY(-100%);
+	}
+}
+
+.tui-ms__item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+</style>

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

@@ -0,0 +1,654 @@
+<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) {
+				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>

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

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

+ 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: 9999
+		},
+		//mask z-index
+		maskZIndex: {
+			type: [Number, String],
+			default: 9998
+		},
+		backgroundColor: {
+			type: String,
+			default: '#fff'
+		}
+	},
+	methods: {
+		handleMaskClick() {
+			if (!this.maskClosable) {
+				return;
+			}
+			this.$emit('close', {});
+		}
+	}
+};
+</script>
+
+<style scoped>
+.tui-drawer-mask {
+	opacity: 0;
+	visibility: hidden;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	transition: all 0.3s ease-in-out;
+}
+.tui-drawer-mask_show {
+	display: block;
+	visibility: visible;
+	opacity: 1;
+}
+
+.tui-drawer-container {
+	position: fixed;
+	left: 50%;
+	height: 100.2%;
+	top: 0;
+	transform: translate3d(-50%, -50%, 0);
+	transform-origin: center;
+	transition: all 0.3s ease-in-out;
+	opacity: 0;
+	overflow-y: scroll;
+	-webkit-overflow-scrolling: touch;
+	-ms-touch-action: pan-y cross-slide-y;
+	-ms-scroll-chaining: none;
+	-ms-scroll-limit: 0 50 0 50;
+}
+.tui-drawer-container_left {
+	left: 0;
+	top: 50%;
+	transform: translate3d(-100%, -50%, 0);
+}
+
+.tui-drawer-container_right {
+	right: 0;
+	top: 50%;
+	left: auto;
+	transform: translate3d(100%, -50%, 0);
+}
+
+.tui-drawer-container_bottom,
+.tui-drawer-container_top {
+	width: 100%;
+	height: auto !important;
+	min-height: 20rpx;
+	left: 0;
+	right: 0;
+	transform-origin: center;
+	transition: all 0.3s ease-in-out;
+}
+.tui-drawer-container_bottom {
+	bottom: 0;
+	top: auto;
+	transform: translate3d(0, 100%, 0);
+}
+.tui-drawer-container_top {
+	transform: translate3d(0, -100%, 0);
+}
+.tui-drawer-left__show,
+.tui-drawer-right__show {
+	opacity: 1;
+	transform: translate3d(0, -50%, 0);
+}
+.tui-drawer-top__show,
+.tui-drawer-bottom__show {
+	opacity: 1;
+	transform: translate3d(0, 0, 0);
+}
+</style>

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

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

+ 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':hidden}">
+				<view class="tui-fab-item-box" :class="{'tui-fab-item-left':left && !right && item.imgUrl}"
+					v-for="(item,index) in btnList" :key="index" @tap.stop="handleClick(index)">
+					<view :class="[left && !right?'tui-text-left':'tui-text-right']" v-if="item.imgUrl"
+						:style="{fontSize:item.fontSize+'rpx',color:item.color}">{{item.text || ""}}</view>
+					<view class="tui-fab-item"
+						:style="{width:width+'rpx',height:height+'rpx',background:item.bgColor || bgColor,borderRadius:radius}">
+						<view class="tui-fab-title" v-if="!item.imgUrl"
+							:style="{fontSize:item.fontSize+'rpx',color:item.color}">{{item.text || ""}}</view>
+						<image :src="item.imgUrl" class="tui-fab-img" v-else
+							:style="{width:item.imgWidth+'rpx',height:item.imgHeight+'rpx'}"></image>
+					</view>
+				</view>
+			</view>
+			<view class="tui-fab-item" :class="{'tui-active':isOpen}"
+				:style="{width:width+'rpx',height:height+'rpx',borderRadius:radius,background:bgColor,color:color}"
+				@tap.stop="handleClick(-1)">
+				<text class="tui-fab-icon tui-icon-plus" v-if="!custom"></text>
+				<slot></slot>
+			</view>
+		</view>
+		<view class="tui-fab-mask" :class="{'tui-visible':isOpen}" @tap="handleClickCancel"></view>
+	</view>
+</template>
+
+<script>
+	//拓展出来的按钮不应多于6个,否则违反了作为悬浮按钮的快速、高效的原则
+	export default {
+		name: "tuiFab",
+		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,
+				hidden: 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.hidden = false
+				clearTimeout(this.timer)
+				if (index == -1 && this.btnList.length) {
+					this.isOpen = !this.isOpen
+				} else {
+					this.$emit("click", {
+						index: index
+					})
+					this.isOpen = false
+				}
+				if (!this.isOpen) {
+					this.timer = setTimeout(() => {
+						this.hidden = true
+					}, 200)
+				}
+			},
+			handleClickCancel: function() {
+				if (!this.maskClosable) return;
+				this.isOpen = false
+			}
+		}
+	}
+</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>

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

@@ -0,0 +1,148 @@
+<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;
+	}
+
+	.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


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

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

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

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

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

@@ -0,0 +1,173 @@
+<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;
+}
+
+.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>

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

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

+ 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 />
+	</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: 9998
+			}
+		},
+		watch: {
+			scrollTop(newValue, oldValue) {
+				if (this.isOpacity && !this.transparent) {
+					this.opacityChange();
+				}
+			},
+			backgroundColor(val) {
+				if (val) {
+					this.background = this.hexToRgb(val);
+				}
+			}
+		},
+		data() {
+			return {
+				width: 375, //header宽度
+				left: 375, //小程序端 左侧距胶囊按钮距离
+				height: 44, //header高度
+				top: 0,
+				scrollH: 1, //滚动总高度,计算opacity
+				opacity: 1, //0-1
+				statusBarHeight: 0, //状态栏高度
+				background: '255,255,255', //header背景色
+				dropDownOpacity: 1
+			};
+		},
+		created() {
+			this.dropDownOpacity = this.backdropFilter && 0;
+			this.opacity = this.isOpacity || this.transparent ? 0 : this.maxOpacity;
+			this.background = this.hexToRgb(this.backgroundColor);
+			let obj = {};
+			// #ifdef MP-WEIXIN
+			obj = wx.getMenuButtonBoundingClientRect();
+			// #endif
+			// #ifdef MP-BAIDU
+			obj = swan.getMenuButtonBoundingClientRect();
+			// #endif
+			// #ifdef MP-ALIPAY
+			my.hideAddToDesktopMenu();
+			// #endif
+			uni.getSystemInfo({
+				success: res => {
+					this.statusBarHeight = res.statusBarHeight;
+					this.width = res.windowWidth;
+					this.left = obj.left || res.windowWidth;
+					if (this.isImmersive) {
+						this.height = obj.top ? obj.top + obj.height + 8 : res.statusBarHeight + 44;
+					}
+					this.scrollH = res.windowWidth * this.scrollRatio;
+					this.top = obj.top ? obj.top + (obj.height - 32) / 2 : res.statusBarHeight + 6;
+					this.$emit('init', {
+						width: this.width,
+						height: this.height,
+						left: this.left,
+						top: this.top,
+						statusBarHeight: this.statusBarHeight,
+						opacity: this.opacity,
+						windowHeight: res.windowHeight
+					});
+				}
+			});
+		},
+		methods: {
+			hexToRgb(hex) {
+				let rgb = '255,255,255';
+				if (hex && ~hex.indexOf('#')) {
+					if (hex.length === 4) {
+						let text = hex.substring(1, 4);
+						hex = '#' + text + text;
+					}
+					let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+					if (result) {
+						rgb = `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`;
+					}
+				}
+				return rgb;
+			},
+			opacityChange() {
+				if (this.dropDownHide) {
+					if (this.scrollTop < 0) {
+						if (this.dropDownOpacity > 0) {
+							this.dropDownOpacity = 1 - Math.abs(this.scrollTop) / 30;
+						}
+					} else {
+						this.dropDownOpacity = 1;
+					}
+				}
+
+				let scroll = this.scrollTop <= 1 ? 0 : this.scrollTop;
+				let opacity = scroll / this.scrollH;
+				if ((this.opacity >= this.maxOpacity && opacity >= this.maxOpacity) || (this.opacity == 0 && opacity ==
+					0)) {
+					return;
+				}
+				this.opacity = opacity > this.maxOpacity ? this.maxOpacity : opacity;
+				if (this.backdropFilter) {
+					this.dropDownOpacity = this.opacity >= this.maxOpacity ? 1 : this.opacity;
+				}
+				this.$emit('change', {
+					opacity: this.opacity
+				});
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.tui-navigation-bar {
+		width: 100%;
+		transition: opacity 0.4s;
+	}
+
+	.tui-backdrop__filter {
+		/* Safari for macOS & iOS */
+		-webkit-backdrop-filter: blur(15px);
+		/* Google Chrome */
+		backdrop-filter: blur(15px);
+	}
+
+	.tui-navbar-fixed {
+		position: fixed;
+		left: 0;
+		top: 0;
+	}
+
+	.tui-status-bar {
+		width: 100%;
+	}
+
+	.tui-navigation_bar-title {
+		width: 100%;
+		font-size: 17px;
+		line-height: 17px;
+		/* #ifndef APP-PLUS */
+		font-weight: 500;
+		/* #endif */
+		height: 32px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.tui-bar-line::after {
+		content: '';
+		position: absolute;
+		border-bottom: 1rpx solid #eaeef1;
+		-webkit-transform: scaleY(0.5);
+		transform: scaleY(0.5);
+		bottom: 0;
+		right: 0;
+		left: 0;
+	}
+</style>

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

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

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

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

@@ -0,0 +1,700 @@
+<template>
+	<view class="tui-container" @touchmove.stop.prevent="stop">
+		<view
+			class="tui-image-cropper"
+			:change:prop="parse.propsChange"
+			:prop="props"
+			:data-lockRatio="lockRatio"
+			:data-lockWidth="lockWidth"
+			:data-lockHeight="lockHeight"
+			:data-maxWidth="maxWidth"
+			:data-minWidth="minWidth"
+			:data-maxHeight="maxHeight"
+			:data-minHeight="minHeight"
+			:data-width="width"
+			:data-height="height"
+			:data-limitMove="limitMove"
+			:data-windowHeight="sysInfo.windowHeight || 600"
+			:data-windowWidth="sysInfo.windowWidth || 400"
+			:data-imgTop="imgTop"
+			:data-imgLeft="imgLeft"
+			:data-imgWidth="imgWidth"
+			:data-imgHeight="imgHeight"
+			:data-angle="angle"
+			@touchend="parse.cutTouchEnd"
+			@touchstart="parse.cutTouchStart"
+			@touchmove="parse.cutTouchMove"
+		>
+			<view class="tui-content">
+				<view class="tui-content-top tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				<view class="tui-content-middle">
+					<view class="tui-bg-transparent tui-wxs-bg" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+					<view class="tui-cropper-box" :style="{ borderColor: borderColor, transitionProperty: cutAnimation ? '' : 'background' }">
+						<view
+							v-for="(item, index) in 4"
+							:key="index"
+							class="tui-edge"
+							:class="[`tui-${index < 2 ? 'top' : 'bottom'}-${index === 0 || index === 2 ? 'left' : 'right'}`]"
+							:style="{
+								width: edgeWidth,
+								height: edgeWidth,
+								borderColor: edgeColor,
+								borderWidth: edgeBorderWidth,
+								left: index === 0 || index === 2 ? `-${edgeOffsets}` : 'auto',
+								right: index === 1 || index === 3 ? `-${edgeOffsets}` : 'auto',
+								top: index < 2 ? `-${edgeOffsets}` : 'auto',
+								bottom: index > 1 ? `-${edgeOffsets}` : 'auto'
+							}"
+						></view>
+					</view>
+					<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+				</view>
+				<view class="tui-flex-auto tui-bg-transparent" :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
+			</view>
+			<image
+				@load="imageLoad"
+				@error="imageLoad"
+				@touchstart="parse.touchstart"
+				@touchmove="parse.touchmove"
+				@touchend="parse.touchend"
+				:data-minScale="minScale"
+				:data-maxScale="maxScale"
+				:data-disableRotate="disableRotate"
+				:style="{
+					width: imgWidth ? imgWidth + 'px' : 'auto',
+					height: imgHeight ? imgHeight + 'px' : 'auto',
+					transitionDuration: (cutAnimation ? 0.3 : 0) + 's'
+				}"
+				class="tui-cropper-image"
+				:src="imageUrl"
+				v-if="imageUrl"
+				mode="widthFix"
+			></image>
+		</view>
+		<canvas
+			canvas-id="tui-image-cropper"
+			id="tui-image-cropper"
+			:disable-scroll="true"
+			:style="{ width: CROPPER_WIDTH * scaleRatio + 'px', height: CROPPER_HEIGHT * scaleRatio + 'px' }"
+			class="tui-cropper-canvas"
+		></canvas>
+		<view class="tui-cropper-tabbar" v-if="!custom">
+			<view class="tui-op-btn" @tap.stop="back">取消</view>
+			<image :src="rotateImg" class="tui-rotate-img" @tap="setAngle"></image>
+			<view class="tui-op-btn" @tap.stop="getImage">完成</view>
+		</view>
+	</view>
+</template>
+<script src="./tui-picture-cropper.wxs" module="parse" lang="wxs"></script>
+<script>
+/**
+ * 注意:组件中使用的图片地址,将文件复制到自己项目中
+ * 如果图片位置与组件同级,编译成小程序时图片会丢失
+ * 拷贝static下整个components文件夹
+ *也可直接转成base64(不建议)
+ * */
+export default {
+	name: 'tuiPictureCropper',
+	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
+					// #ifdef MP-BAIDU || MP-TOUTIAO || H5
+					this.isBase64 = false;
+					// #endif
+					if (this.isBase64) {
+						uni.canvasGetImageData({
+							canvasId: 'tui-image-cropper',
+							x: 0,
+							y: 0,
+							width: this.canvasWidth * this.scaleRatio,
+							height: Math.round(this.canvasHeight * this.scaleRatio),
+							success: res => {
+								const arrayBuffer = new Uint8Array(res.data);
+								const base64 = uni.arrayBufferToBase64(arrayBuffer);
+								data.base64 = base64;
+								this.loadding && uni.hideLoading();
+								this.$emit('cropper', data);
+							}
+						},this);
+					} else {
+						uni.canvasToTempFilePath(
+							{
+								...params,
+								canvasId: 'tui-image-cropper',
+								success: res => {
+									data.url = res.tempFilePath;
+									// #ifdef H5
+									data.base64 = res.tempFilePath;
+									// #endif
+									this.loadding && uni.hideLoading();
+									this.$emit('cropper', data);
+								},
+								fail(res) {
+									console.log(res);
+								}
+							},
+							this
+						);
+					}
+					// #endif
+				});
+			};
+			if (this.CROPPER_WIDTH != this.canvasWidth || this.CROPPER_HEIGHT != this.canvasHeight) {
+				this.CROPPER_WIDTH = this.canvasWidth;
+				this.CROPPER_HEIGHT = this.canvasHeight;
+				this.$nextTick(() => {
+					this.ctx.draw();
+					setTimeout(() => {
+						draw();
+					}, 100);
+				});
+			} else {
+				draw();
+			}
+		},
+		change(e) {
+			this.cutX = e.cutX || 0;
+			this.cutY = e.cutY || 0;
+			this.canvasWidth = e.canvasWidth || this.width;
+			this.canvasHeight = e.canvasHeight || this.height;
+			this.imgWidth = e.imgWidth || this.imgWidth;
+			this.imgHeight = e.imgHeight || this.imgHeight;
+			this.scale = e.scale || 1;
+			this.angle = e.angle || 0;
+			this.imgTop = e.imgTop || 0;
+			this.imgLeft = e.imgLeft || 0;
+		},
+		imageReset() {
+			this.scale = 1;
+			this.angle = 0;
+			let sys = this.sysInfo.windowHeight ? this.sysInfo : uni.getSystemInfoSync();
+			this.imgTop = sys.windowHeight / 2;
+			this.imgLeft = sys.windowWidth / 2;
+			this.resetChange++;
+			this.props = `4,${this.resetChange}`;
+			//初始化旋转角度 0deg
+			this.$emit('initAngle', {});
+		},
+		imageLoad(e) {
+			this.imageReset();
+			uni.hideLoading();
+			this.$emit('imageLoad', {});
+		},
+
+		imgComputeSize(width, height) {
+			//默认按图片最小边 = 对应裁剪框尺寸
+			let imgWidth = width,
+				imgHeight = height;
+			if (imgWidth && imgHeight) {
+				if (imgWidth / imgHeight > this.width / this.height) {
+					imgHeight = this.height;
+					imgWidth = (width / height) * imgHeight;
+				} else {
+					imgWidth = this.width;
+					imgHeight = (height / width) * imgWidth;
+				}
+			} else {
+				let sys = this.sysInfo.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;
+	background-color: rgba(0, 0, 0, 0.6);
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 1;
+}
+
+.tui-image-cropper {
+	width: 100vw;
+	height: 100vh;
+	position: absolute;
+}
+
+.tui-content {
+	width: 100vw;
+	height: 100vh;
+	position: absolute;
+	z-index: 9;
+	display: flex;
+	flex-direction: column;
+	pointer-events: none;
+}
+
+.tui-bg-transparent {
+	background-color: rgba(0, 0, 0, 0.6);
+	transition-duration: 0.3s;
+}
+
+.tui-content-top {
+	pointer-events: none;
+}
+
+.tui-content-middle {
+	width: 100%;
+	height: 200px;
+	display: flex;
+	box-sizing: border-box;
+}
+
+.tui-cropper-box {
+	position: relative;
+	/* transition-duration: 0.2s; */
+	border-style: solid;
+	border-width: 1rpx;
+	box-sizing: border-box;
+}
+
+.tui-flex-auto {
+	flex: auto;
+}
+
+.tui-cropper-image {
+	width: 100%;
+	border-style: none;
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 2;
+	-webkit-backface-visibility: hidden;
+	backface-visibility: hidden;
+	transform-origin: center;
+}
+
+.tui-cropper-canvas {
+	position: fixed;
+	z-index: 10;
+	left: -2000px;
+	top: -2000px;
+	pointer-events: none;
+}
+
+.tui-edge {
+	border-style: solid;
+	pointer-events: auto;
+	position: absolute;
+	box-sizing: border-box;
+}
+
+.tui-top-left {
+	border-bottom-width: 0 !important;
+	border-right-width: 0 !important;
+}
+
+.tui-top-right {
+	border-bottom-width: 0 !important;
+	border-left-width: 0 !important;
+}
+
+.tui-bottom-left {
+	border-top-width: 0 !important;
+	border-right-width: 0 !important;
+}
+
+.tui-bottom-right {
+	border-top-width: 0 !important;
+	border-left-width: 0 !important;
+}
+
+.tui-cropper-tabbar {
+	width: 100%;
+	height: 120rpx;
+	padding: 0 40rpx;
+	box-sizing: border-box;
+	position: fixed;
+	left: 0;
+	bottom: 0;
+	z-index: 99;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	color: #ffffff;
+	font-size: 32rpx;
+}
+
+.tui-cropper-tabbar::after {
+	content: ' ';
+	position: absolute;
+	top: 0;
+	right: 0;
+	left: 0;
+	border-top: 1rpx solid rgba(255, 255, 255, 0.2);
+	-webkit-transform: scaleY(0.5) translateZ(0);
+	transform: scaleY(0.5) translateZ(0);
+	transform-origin: 0 100%;
+}
+
+.tui-op-btn {
+	height: 80rpx;
+	display: flex;
+	align-items: center;
+}
+
+.tui-rotate-img {
+	width: 44rpx;
+	height: 44rpx;
+}
+</style>

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

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

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

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

+ 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: 20rpx;
+}
+
+.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>

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

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

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

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

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

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

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

@@ -0,0 +1,155 @@
+<template>
+	<view class="tui-sticky-class">
+		<!--sticky 容器-->
+		<view :style="{height: stickyHeight,backgroundColor:backgroundColor}" v-if="isFixed"></view>
+		<view :class="{'tui-sticky-fixed':isFixed}" :style="{top:isFixed?stickyTop+'px':'auto'}">
+			<slot name="header"></slot>
+		</view>
+		<!--sticky 容器-->
+		<!--内容-->
+		<slot name="content"></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "tuiSticky",
+		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();
+			}, 20)
+		},
+		data() {
+			return {
+				timer: null,
+				top: 0,
+				height: 0,
+				isFixed: false,
+				initialize: 0 //重新初始化
+			};
+		},
+		methods: {
+			updateStickyChange() {
+				const top = this.top;
+				const height = this.height;
+				const scrollTop = this.scrollTop
+				let stickyTop = this.stickyTop
+				// #ifdef H5
+				if (this.isNativeHeader) {
+					stickyTop = stickyTop - 44
+					stickyTop = stickyTop < 0 ? 0 : stickyTop
+				}
+				// #endif
+				if (this.container) {
+					this.isFixed = (scrollTop + stickyTop >= top && scrollTop + stickyTop < top + height) ? true : false
+				} else {
+					this.isFixed = scrollTop + stickyTop >= top ? true : false
+				}
+				//是否吸顶
+				this.$emit("sticky", {
+					isFixed: this.isFixed,
+					index: this.index
+				})
+			},
+			updateScrollChange(callback) {
+				if (this.timer) {
+					clearTimeout(this.timer)
+					this.timer = null
+				}
+				this.timer = setTimeout(() => {
+					const className = '.tui-sticky-class';
+					const query = uni.createSelectorQuery().in(this);
+					query.select(className).boundingClientRect((res) => {
+						if (res) {
+							this.top = res.top + (this.scrollTop || 0);
+							this.height = res.height;
+							callback && callback();
+							this.$emit("change", {
+								index: Number(this.index),
+								top: this.top
+							})
+						}
+					}).exec()
+				}, 0)
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.tui-sticky-fixed {
+		width: 100%;
+		position: fixed;
+		left: 0;
+		z-index: 888;
+	}
+</style>

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

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

@@ -0,0 +1,280 @@
+<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>

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

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

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

@@ -0,0 +1,500 @@
+<template>
+	<view class="tui-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: 999;
+	}
+
+	.tui-img-del::before {
+		content: '';
+		width: 16rpx;
+		height: 1px;
+		position: absolute;
+		left: 10rpx;
+		top: 18rpx;
+		background-color: #fff;
+	}
+
+	.tui-upload-mask {
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		left: 0;
+		top: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: 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>

+ 3 - 0
components/uni/marked/index.js

@@ -0,0 +1,3 @@
+// module.exports = require('./lib/marked');
+import marked from './lib/marked'
+export default marked

+ 1573 - 0
components/uni/marked/lib/marked.js

@@ -0,0 +1,1573 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/markedjs/marked
+ */
+
+;(function(root) {
+'use strict';
+
+/**
+ * Block-Level Grammar
+ */
+
+var block = {
+  newline: /^\n+/,
+  code: /^( {4}[^\n]+\n*)+/,
+  fences: noop,
+  hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,
+  heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,
+  nptable: noop,
+  blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,
+  list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
+  html: '^ {0,3}(?:' // optional indentation
+    + '<(script|pre|style)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)' // (1)
+    + '|comment[^\\n]*(\\n+|$)' // (2)
+    + '|<\\?[\\s\\S]*?\\?>\\n*' // (3)
+    + '|<![A-Z][\\s\\S]*?>\\n*' // (4)
+    + '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>\\n*' // (5)
+    + '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:\\n{2,}|$)' // (6)
+    + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag
+    + '|</(?!script|pre|style)[a-z][\\w-]*\\s*>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag
+    + ')',
+  def: /^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,
+  table: noop,
+  lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
+  paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/,
+  text: /^[^\n]+/
+};
+
+block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/;
+block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/;
+block.def = edit(block.def)
+  .replace('label', block._label)
+  .replace('title', block._title)
+  .getRegex();
+
+block.bullet = /(?:[*+-]|\d+\.)/;
+block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
+block.item = edit(block.item, 'gm')
+  .replace(/bull/g, block.bullet)
+  .getRegex();
+
+block.list = edit(block.list)
+  .replace(/bull/g, block.bullet)
+  .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))')
+  .replace('def', '\\n+(?=' + block.def.source + ')')
+  .getRegex();
+
+block._tag = 'address|article|aside|base|basefont|blockquote|body|caption'
+  + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption'
+  + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe'
+  + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option'
+  + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr'
+  + '|track|ul';
+block._comment = /<!--(?!-?>)[\s\S]*?-->/;
+block.html = edit(block.html, 'i')
+  .replace('comment', block._comment)
+  .replace('tag', block._tag)
+  .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/)
+  .getRegex();
+
+block.paragraph = edit(block.paragraph)
+  .replace('hr', block.hr)
+  .replace('heading', block.heading)
+  .replace('lheading', block.lheading)
+  .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks
+  .getRegex();
+
+block.blockquote = edit(block.blockquote)
+  .replace('paragraph', block.paragraph)
+  .getRegex();
+
+/**
+ * Normal Block Grammar
+ */
+
+block.normal = merge({}, block);
+
+/**
+ * GFM Block Grammar
+ */
+
+block.gfm = merge({}, block.normal, {
+  fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\n? *\1 *(?:\n+|$)/,
+  paragraph: /^/,
+  heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/
+});
+
+block.gfm.paragraph = edit(block.paragraph)
+  .replace('(?!', '(?!'
+    + block.gfm.fences.source.replace('\\1', '\\2') + '|'
+    + block.list.source.replace('\\1', '\\3') + '|')
+  .getRegex();
+
+/**
+ * GFM + Tables Block Grammar
+ */
+
+block.tables = merge({}, block.gfm, {
+  nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,
+  table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/
+});
+
+/**
+ * Pedantic grammar
+ */
+
+block.pedantic = merge({}, block.normal, {
+  html: edit(
+    '^ *(?:comment *(?:\\n|\\s*$)'
+    + '|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)' // closed tag
+    + '|<tag(?:"[^"]*"|\'[^\']*\'|\\s[^\'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))')
+    .replace('comment', block._comment)
+    .replace(/tag/g, '(?!(?:'
+      + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub'
+      + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)'
+      + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b')
+    .getRegex(),
+  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/
+});
+
+/**
+ * Block Lexer
+ */
+
+function Lexer(options) {
+  this.tokens = [];
+  this.tokens.links = Object.create(null);
+  this.options = options || marked.defaults;
+  this.rules = block.normal;
+
+  if (this.options.pedantic) {
+    this.rules = block.pedantic;
+  } else if (this.options.gfm) {
+    if (this.options.tables) {
+      this.rules = block.tables;
+    } else {
+      this.rules = block.gfm;
+    }
+  }
+}
+
+/**
+ * Expose Block Rules
+ */
+
+Lexer.rules = block;
+
+/**
+ * Static Lex Method
+ */
+
+Lexer.lex = function(src, options) {
+  var lexer = new Lexer(options);
+  return lexer.lex(src);
+};
+
+/**
+ * Preprocessing
+ */
+
+Lexer.prototype.lex = function(src) {
+  src = src
+    .replace(/\r\n|\r/g, '\n')
+    .replace(/\t/g, '    ')
+    .replace(/\u00a0/g, ' ')
+    .replace(/\u2424/g, '\n');
+
+  return this.token(src, true);
+};
+
+/**
+ * Lexing
+ */
+
+Lexer.prototype.token = function(src, top) {
+  src = src.replace(/^ +$/gm, '');
+  var next,
+      loose,
+      cap,
+      bull,
+      b,
+      item,
+      listStart,
+      listItems,
+      t,
+      space,
+      i,
+      tag,
+      l,
+      isordered,
+      istask,
+      ischecked;
+
+  while (src) {
+    // newline
+    if (cap = this.rules.newline.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[0].length > 1) {
+        this.tokens.push({
+          type: 'space'
+        });
+      }
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      cap = cap[0].replace(/^ {4}/gm, '');
+      this.tokens.push({
+        type: 'code',
+        text: !this.options.pedantic
+          ? rtrim(cap, '\n')
+          : cap
+      });
+      continue;
+    }
+
+    // fences (gfm)
+    if (cap = this.rules.fences.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'code',
+        lang: cap[2],
+        text: cap[3] || ''
+      });
+      continue;
+    }
+
+    // heading
+    if (cap = this.rules.heading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[1].length,
+        text: cap[2]
+      });
+      continue;
+    }
+
+    // table no leading pipe (gfm)
+    if (top && (cap = this.rules.nptable.exec(src))) {
+      item = {
+        type: 'table',
+        header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
+      };
+
+      if (item.header.length === item.align.length) {
+        src = src.substring(cap[0].length);
+
+        for (i = 0; i < item.align.length; i++) {
+          if (/^ *-+: *$/.test(item.align[i])) {
+            item.align[i] = 'right';
+          } else if (/^ *:-+: *$/.test(item.align[i])) {
+            item.align[i] = 'center';
+          } else if (/^ *:-+ *$/.test(item.align[i])) {
+            item.align[i] = 'left';
+          } else {
+            item.align[i] = null;
+          }
+        }
+
+        for (i = 0; i < item.cells.length; i++) {
+          item.cells[i] = splitCells(item.cells[i], item.header.length);
+        }
+
+        this.tokens.push(item);
+
+        continue;
+      }
+    }
+
+    // hr
+    if (cap = this.rules.hr.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'hr'
+      });
+      continue;
+    }
+
+    // blockquote
+    if (cap = this.rules.blockquote.exec(src)) {
+      src = src.substring(cap[0].length);
+
+      this.tokens.push({
+        type: 'blockquote_start'
+      });
+
+      cap = cap[0].replace(/^ *> ?/gm, '');
+
+      // Pass `top` to keep the current
+      // "toplevel" state. This is exactly
+      // how markdown.pl works.
+      this.token(cap, top);
+
+      this.tokens.push({
+        type: 'blockquote_end'
+      });
+
+      continue;
+    }
+
+    // list
+    if (cap = this.rules.list.exec(src)) {
+      src = src.substring(cap[0].length);
+      bull = cap[2];
+      isordered = bull.length > 1;
+
+      listStart = {
+        type: 'list_start',
+        ordered: isordered,
+        start: isordered ? +bull : '',
+        loose: false
+      };
+
+      this.tokens.push(listStart);
+
+      // Get each top-level item.
+      cap = cap[0].match(this.rules.item);
+
+      listItems = [];
+      next = false;
+      l = cap.length;
+      i = 0;
+
+      for (; i < l; i++) {
+        item = cap[i];
+
+        // Remove the list item's bullet
+        // so it is seen as the next token.
+        space = item.length;
+        item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+        // Outdent whatever the
+        // list item contains. Hacky.
+        if (~item.indexOf('\n ')) {
+          space -= item.length;
+          item = !this.options.pedantic
+            ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
+            : item.replace(/^ {1,4}/gm, '');
+        }
+
+        // Determine whether the next list item belongs here.
+        // Backpedal if it does not belong in this list.
+        if (this.options.smartLists && i !== l - 1) {
+          b = block.bullet.exec(cap[i + 1])[0];
+          if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+            src = cap.slice(i + 1).join('\n') + src;
+            i = l - 1;
+          }
+        }
+
+        // Determine whether item is loose or not.
+        // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+        // for discount behavior.
+        loose = next || /\n\n(?!\s*$)/.test(item);
+        if (i !== l - 1) {
+          next = item.charAt(item.length - 1) === '\n';
+          if (!loose) loose = next;
+        }
+
+        if (loose) {
+          listStart.loose = true;
+        }
+
+        // Check for task list items
+        istask = /^\[[ xX]\] /.test(item);
+        ischecked = undefined;
+        if (istask) {
+          ischecked = item[1] !== ' ';
+          item = item.replace(/^\[[ xX]\] +/, '');
+        }
+
+        t = {
+          type: 'list_item_start',
+          task: istask,
+          checked: ischecked,
+          loose: loose
+        };
+
+        listItems.push(t);
+        this.tokens.push(t);
+
+        // Recurse.
+        this.token(item, false);
+
+        this.tokens.push({
+          type: 'list_item_end'
+        });
+      }
+
+      if (listStart.loose) {
+        l = listItems.length;
+        i = 0;
+        for (; i < l; i++) {
+          listItems[i].loose = true;
+        }
+      }
+
+      this.tokens.push({
+        type: 'list_end'
+      });
+
+      continue;
+    }
+
+    // html
+    if (cap = this.rules.html.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: this.options.sanitize
+          ? 'paragraph'
+          : 'html',
+        pre: !this.options.sanitizer
+          && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+        text: cap[0]
+      });
+      continue;
+    }
+
+    // def
+    if (top && (cap = this.rules.def.exec(src))) {
+      src = src.substring(cap[0].length);
+      if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1);
+      tag = cap[1].toLowerCase().replace(/\s+/g, ' ');
+      if (!this.tokens.links[tag]) {
+        this.tokens.links[tag] = {
+          href: cap[2],
+          title: cap[3]
+        };
+      }
+      continue;
+    }
+
+    // table (gfm)
+    if (top && (cap = this.rules.table.exec(src))) {
+      item = {
+        type: 'table',
+        header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3] ? cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') : []
+      };
+
+      if (item.header.length === item.align.length) {
+        src = src.substring(cap[0].length);
+
+        for (i = 0; i < item.align.length; i++) {
+          if (/^ *-+: *$/.test(item.align[i])) {
+            item.align[i] = 'right';
+          } else if (/^ *:-+: *$/.test(item.align[i])) {
+            item.align[i] = 'center';
+          } else if (/^ *:-+ *$/.test(item.align[i])) {
+            item.align[i] = 'left';
+          } else {
+            item.align[i] = null;
+          }
+        }
+
+        for (i = 0; i < item.cells.length; i++) {
+          item.cells[i] = splitCells(
+            item.cells[i].replace(/^ *\| *| *\| *$/g, ''),
+            item.header.length);
+        }
+
+        this.tokens.push(item);
+
+        continue;
+      }
+    }
+
+    // lheading
+    if (cap = this.rules.lheading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[2] === '=' ? 1 : 2,
+        text: cap[1]
+      });
+      continue;
+    }
+
+    // top-level paragraph
+    if (top && (cap = this.rules.paragraph.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'paragraph',
+        text: cap[1].charAt(cap[1].length - 1) === '\n'
+          ? cap[1].slice(0, -1)
+          : cap[1]
+      });
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      // Top-level should never reach here.
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'text',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    if (src) {
+      throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return this.tokens;
+};
+
+/**
+ * Inline-Level Grammar
+ */
+
+var inline = {
+  escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,
+  autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/,
+  url: noop,
+  tag: '^comment'
+    + '|^</[a-zA-Z][\\w:-]*\\s*>' // self-closing tag
+    + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag
+    + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. <?php ?>
+    + '|^<![a-zA-Z]+\\s[\\s\\S]*?>' // declaration, e.g. <!DOCTYPE html>
+    + '|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>', // CDATA section
+  link: /^!?\[(label)\]\(href(?:\s+(title))?\s*\)/,
+  reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,
+  nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,
+  strong: /^__([^\s])__(?!_)|^\*\*([^\s])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,
+  em: /^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_)|^_([^\s_][\s\S]*?[^\s])_(?!_)|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,
+  code: /^(`+)\s*([\s\S]*?[^`]?)\s*\1(?!`)/,
+  br: /^( {2,}|\\)\n(?!\s*$)/,
+  del: noop,
+  text: /^[\s\S]+?(?=[\\<!\[`*]|\b_| {2,}\n|$)/
+};
+
+inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g;
+
+inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/;
+inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/;
+inline.autolink = edit(inline.autolink)
+  .replace('scheme', inline._scheme)
+  .replace('email', inline._email)
+  .getRegex();
+
+inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/;
+
+inline.tag = edit(inline.tag)
+  .replace('comment', block._comment)
+  .replace('attribute', inline._attribute)
+  .getRegex();
+
+inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/;
+inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f\\]*\)|[^\s\x00-\x1f()\\])*?)/;
+inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/;
+
+inline.link = edit(inline.link)
+  .replace('label', inline._label)
+  .replace('href', inline._href)
+  .replace('title', inline._title)
+  .getRegex();
+
+inline.reflink = edit(inline.reflink)
+  .replace('label', inline._label)
+  .getRegex();
+
+/**
+ * Normal Inline Grammar
+ */
+
+inline.normal = merge({}, inline);
+
+/**
+ * Pedantic Inline Grammar
+ */
+
+inline.pedantic = merge({}, inline.normal, {
+  strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
+  em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,
+  link: edit(/^!?\[(label)\]\((.*?)\)/)
+    .replace('label', inline._label)
+    .getRegex(),
+  reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/)
+    .replace('label', inline._label)
+    .getRegex()
+});
+
+/**
+ * GFM Inline Grammar
+ */
+
+inline.gfm = merge({}, inline.normal, {
+  escape: edit(inline.escape).replace('])', '~|])').getRegex(),
+  url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/)
+    .replace('email', inline._email)
+    .getRegex(),
+  _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,
+  del: /^~+(?=\S)([\s\S]*?\S)~+/,
+  text: edit(inline.text)
+    .replace(']|', '~]|')
+    .replace('|', '|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&\'*+/=?^_`{\\|}~-]+@|')
+    .getRegex()
+});
+
+/**
+ * GFM + Line Breaks Inline Grammar
+ */
+
+inline.breaks = merge({}, inline.gfm, {
+  br: edit(inline.br).replace('{2,}', '*').getRegex(),
+  text: edit(inline.gfm.text).replace('{2,}', '*').getRegex()
+});
+
+/**
+ * Inline Lexer & Compiler
+ */
+
+function InlineLexer(links, options) {
+  this.options = options || marked.defaults;
+  this.links = links;
+  this.rules = inline.normal;
+  this.renderer = this.options.renderer || new Renderer();
+  this.renderer.options = this.options;
+
+  if (!this.links) {
+    throw new Error('Tokens array requires a `links` property.');
+  }
+
+  if (this.options.pedantic) {
+    this.rules = inline.pedantic;
+  } else if (this.options.gfm) {
+    if (this.options.breaks) {
+      this.rules = inline.breaks;
+    } else {
+      this.rules = inline.gfm;
+    }
+  }
+}
+
+/**
+ * Expose Inline Rules
+ */
+
+InlineLexer.rules = inline;
+
+/**
+ * Static Lexing/Compiling Method
+ */
+
+InlineLexer.output = function(src, links, options) {
+  var inline = new InlineLexer(links, options);
+  return inline.output(src);
+};
+
+/**
+ * Lexing/Compiling
+ */
+
+InlineLexer.prototype.output = function(src) {
+  var out = '',
+      link,
+      text,
+      href,
+      title,
+      cap,
+      prevCapZero;
+
+  while (src) {
+    // escape
+    if (cap = this.rules.escape.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += cap[1];
+      continue;
+    }
+
+    // autolink
+    if (cap = this.rules.autolink.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[2] === '@') {
+        text = escape(this.mangle(cap[1]));
+        href = 'mailto:' + text;
+      } else {
+        text = escape(cap[1]);
+        href = text;
+      }
+      out += this.renderer.link(href, null, text);
+      continue;
+    }
+
+    // url (gfm)
+    if (!this.inLink && (cap = this.rules.url.exec(src))) {
+      do {
+        prevCapZero = cap[0];
+        cap[0] = this.rules._backpedal.exec(cap[0])[0];
+      } while (prevCapZero !== cap[0]);
+      src = src.substring(cap[0].length);
+      if (cap[2] === '@') {
+        text = escape(cap[0]);
+        href = 'mailto:' + text;
+      } else {
+        text = escape(cap[0]);
+        if (cap[1] === 'www.') {
+          href = 'http://' + text;
+        } else {
+          href = text;
+        }
+      }
+      out += this.renderer.link(href, null, text);
+      continue;
+    }
+
+    // tag
+    if (cap = this.rules.tag.exec(src)) {
+      if (!this.inLink && /^<a /i.test(cap[0])) {
+        this.inLink = true;
+      } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
+        this.inLink = false;
+      }
+      src = src.substring(cap[0].length);
+      out += this.options.sanitize
+        ? this.options.sanitizer
+          ? this.options.sanitizer(cap[0])
+          : escape(cap[0])
+        : cap[0]
+      continue;
+    }
+
+    // link
+    if (cap = this.rules.link.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.inLink = true;
+      href = cap[2];
+      if (this.options.pedantic) {
+        link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href);
+
+        if (link) {
+          href = link[1];
+          title = link[3];
+        } else {
+          title = '';
+        }
+      } else {
+        title = cap[3] ? cap[3].slice(1, -1) : '';
+      }
+      href = href.trim().replace(/^<([\s\S]*)>$/, '$1');
+      out += this.outputLink(cap, {
+        href: InlineLexer.escapes(href),
+        title: InlineLexer.escapes(title)
+      });
+      this.inLink = false;
+      continue;
+    }
+
+    // reflink, nolink
+    if ((cap = this.rules.reflink.exec(src))
+        || (cap = this.rules.nolink.exec(src))) {
+      src = src.substring(cap[0].length);
+      link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
+      link = this.links[link.toLowerCase()];
+      if (!link || !link.href) {
+        out += cap[0].charAt(0);
+        src = cap[0].substring(1) + src;
+        continue;
+      }
+      this.inLink = true;
+      out += this.outputLink(cap, link);
+      this.inLink = false;
+      continue;
+    }
+
+    // strong
+    if (cap = this.rules.strong.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1]));
+      continue;
+    }
+
+    // em
+    if (cap = this.rules.em.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1]));
+      continue;
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.codespan(escape(cap[2].trim(), true));
+      continue;
+    }
+
+    // br
+    if (cap = this.rules.br.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.br();
+      continue;
+    }
+
+    // del (gfm)
+    if (cap = this.rules.del.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.del(this.output(cap[1]));
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.text(escape(this.smartypants(cap[0])));
+      continue;
+    }
+
+    if (src) {
+      throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return out;
+};
+
+InlineLexer.escapes = function(text) {
+  return text ? text.replace(InlineLexer.rules._escapes, '$1') : text;
+}
+
+/**
+ * Compile Link
+ */
+
+InlineLexer.prototype.outputLink = function(cap, link) {
+  var href = link.href,
+      title = link.title ? escape(link.title) : null;
+
+  return cap[0].charAt(0) !== '!'
+    ? this.renderer.link(href, title, this.output(cap[1]))
+    : this.renderer.image(href, title, escape(cap[1]));
+};
+
+/**
+ * Smartypants Transformations
+ */
+
+InlineLexer.prototype.smartypants = function(text) {
+  if (!this.options.smartypants) return text;
+  return text
+    // em-dashes
+    .replace(/---/g, '\u2014')
+    // en-dashes
+    .replace(/--/g, '\u2013')
+    // opening singles
+    .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
+    // closing singles & apostrophes
+    .replace(/'/g, '\u2019')
+    // opening doubles
+    .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
+    // closing doubles
+    .replace(/"/g, '\u201d')
+    // ellipses
+    .replace(/\.{3}/g, '\u2026');
+};
+
+/**
+ * Mangle Links
+ */
+
+InlineLexer.prototype.mangle = function(text) {
+  if (!this.options.mangle) return text;
+  var out = '',
+      l = text.length,
+      i = 0,
+      ch;
+
+  for (; i < l; i++) {
+    ch = text.charCodeAt(i);
+    if (Math.random() > 0.5) {
+      ch = 'x' + ch.toString(16);
+    }
+    out += '&#' + ch + ';';
+  }
+
+  return out;
+};
+
+/**
+ * Renderer
+ */
+
+function Renderer(options) {
+  this.options = options || marked.defaults;
+}
+
+Renderer.prototype.code = function(code, lang, escaped) {
+  if (this.options.highlight) {
+    var out = this.options.highlight(code, lang);
+    if (out != null && out !== code) {
+      escaped = true;
+      code = out;
+    }
+  }
+
+  if (!lang) {
+    return '<pre><code>'
+      + (escaped ? code : escape(code, true))
+      + '</code></pre>';
+  }
+
+  return '<pre><code class="'
+    + this.options.langPrefix
+    + escape(lang, true)
+    + '">'
+    + (escaped ? code : escape(code, true))
+    + '</code></pre>\n';
+};
+
+Renderer.prototype.blockquote = function(quote) {
+  return '<blockquote>\n' + quote + '</blockquote>\n';
+};
+
+Renderer.prototype.html = function(html) {
+  return html;
+};
+
+Renderer.prototype.heading = function(text, level, raw) {
+  if (this.options.headerIds) {
+    return '<h'
+      + level
+      + ' id="'
+      + this.options.headerPrefix
+      + raw.toLowerCase().replace(/[^\w]+/g, '-')
+      + '">'
+      + text
+      + '</h'
+      + level
+      + '>\n';
+  }
+  // ignore IDs
+  return '<h' + level + '>' + text + '</h' + level + '>\n';
+};
+
+Renderer.prototype.hr = function() {
+  return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
+};
+
+Renderer.prototype.list = function(body, ordered, start) {
+  var type = ordered ? 'ol' : 'ul',
+      startatt = (ordered && start !== 1) ? (' start="' + start + '"') : '';
+  return '<' + type + startatt + '>\n' + body + '</' + type + '>\n';
+};
+
+Renderer.prototype.listitem = function(text) {
+  return '<li>' + text + '</li>\n';
+};
+
+Renderer.prototype.checkbox = function(checked) {
+  return '<input '
+    + (checked ? 'checked="" ' : '')
+    + 'disabled="" type="checkbox"'
+    + (this.options.xhtml ? ' /' : '')
+    + '> ';
+}
+
+Renderer.prototype.paragraph = function(text) {
+  return '<p>' + text + '</p>\n';
+};
+
+Renderer.prototype.table = function(header, body) {
+  if (body) body = '<tbody>' + body + '</tbody>';
+
+  return '<table>\n'
+    + '<thead>\n'
+    + header
+    + '</thead>\n'
+    + body
+    + '</table>\n';
+};
+
+Renderer.prototype.tablerow = function(content) {
+  return '<tr>\n' + content + '</tr>\n';
+};
+
+Renderer.prototype.tablecell = function(content, flags) {
+  var type = flags.header ? 'th' : 'td';
+  var tag = flags.align
+    ? '<' + type + ' align="' + flags.align + '">'
+    : '<' + type + '>';
+  return tag + content + '</' + type + '>\n';
+};
+
+// span level renderer
+Renderer.prototype.strong = function(text) {
+  return '<strong>' + text + '</strong>';
+};
+
+Renderer.prototype.em = function(text) {
+  return '<em>' + text + '</em>';
+};
+
+Renderer.prototype.codespan = function(text) {
+  return '<code>' + text + '</code>';
+};
+
+Renderer.prototype.br = function() {
+  return this.options.xhtml ? '<br/>' : '<br>';
+};
+
+Renderer.prototype.del = function(text) {
+  return '<del>' + text + '</del>';
+};
+
+Renderer.prototype.link = function(href, title, text) {
+  if (this.options.sanitize) {
+    try {
+      var prot = decodeURIComponent(unescape(href))
+        .replace(/[^\w:]/g, '')
+        .toLowerCase();
+    } catch (e) {
+      return text;
+    }
+    if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
+      return text;
+    }
+  }
+  if (this.options.baseUrl && !originIndependentUrl.test(href)) {
+    href = resolveUrl(this.options.baseUrl, href);
+  }
+  try {
+    href = encodeURI(href).replace(/%25/g, '%');
+  } catch (e) {
+    return text;
+  }
+  var out = '<a href="' + escape(href) + '"';
+  if (title) {
+    out += ' title="' + title + '"';
+  }
+  out += '>' + text + '</a>';
+  return out;
+};
+
+Renderer.prototype.image = function(href, title, text) {
+  if (this.options.baseUrl && !originIndependentUrl.test(href)) {
+    href = resolveUrl(this.options.baseUrl, href);
+  }
+  var out = '<img src="' + href + '" alt="' + text + '"';
+  if (title) {
+    out += ' title="' + title + '"';
+  }
+  out += this.options.xhtml ? '/>' : '>';
+  return out;
+};
+
+Renderer.prototype.text = function(text) {
+  return text;
+};
+
+/**
+ * TextRenderer
+ * returns only the textual part of the token
+ */
+
+function TextRenderer() {}
+
+// no need for block level renderers
+
+TextRenderer.prototype.strong =
+TextRenderer.prototype.em =
+TextRenderer.prototype.codespan =
+TextRenderer.prototype.del =
+TextRenderer.prototype.text = function (text) {
+  return text;
+}
+
+TextRenderer.prototype.link =
+TextRenderer.prototype.image = function(href, title, text) {
+  return '' + text;
+}
+
+TextRenderer.prototype.br = function() {
+  return '';
+}
+
+/**
+ * Parsing & Compiling
+ */
+
+function Parser(options) {
+  this.tokens = [];
+  this.token = null;
+  this.options = options || marked.defaults;
+  this.options.renderer = this.options.renderer || new Renderer();
+  this.renderer = this.options.renderer;
+  this.renderer.options = this.options;
+}
+
+/**
+ * Static Parse Method
+ */
+
+Parser.parse = function(src, options) {
+  var parser = new Parser(options);
+  return parser.parse(src);
+};
+
+/**
+ * Parse Loop
+ */
+
+Parser.prototype.parse = function(src) {
+  this.inline = new InlineLexer(src.links, this.options);
+  // use an InlineLexer with a TextRenderer to extract pure text
+  this.inlineText = new InlineLexer(
+    src.links,
+    merge({}, this.options, {renderer: new TextRenderer()})
+  );
+  this.tokens = src.reverse();
+
+  var out = '';
+  while (this.next()) {
+    out += this.tok();
+  }
+
+  return out;
+};
+
+/**
+ * Next Token
+ */
+
+Parser.prototype.next = function() {
+  return this.token = this.tokens.pop();
+};
+
+/**
+ * Preview Next Token
+ */
+
+Parser.prototype.peek = function() {
+  return this.tokens[this.tokens.length - 1] || 0;
+};
+
+/**
+ * Parse Text Tokens
+ */
+
+Parser.prototype.parseText = function() {
+  var body = this.token.text;
+
+  while (this.peek().type === 'text') {
+    body += '\n' + this.next().text;
+  }
+
+  return this.inline.output(body);
+};
+
+/**
+ * Parse Current Token
+ */
+
+Parser.prototype.tok = function() {
+  switch (this.token.type) {
+    case 'space': {
+      return '';
+    }
+    case 'hr': {
+      return this.renderer.hr();
+    }
+    case 'heading': {
+      return this.renderer.heading(
+        this.inline.output(this.token.text),
+        this.token.depth,
+        unescape(this.inlineText.output(this.token.text)));
+    }
+    case 'code': {
+      return this.renderer.code(this.token.text,
+        this.token.lang,
+        this.token.escaped);
+    }
+    case 'table': {
+      var header = '',
+          body = '',
+          i,
+          row,
+          cell,
+          j;
+
+      // header
+      cell = '';
+      for (i = 0; i < this.token.header.length; i++) {
+        cell += this.renderer.tablecell(
+          this.inline.output(this.token.header[i]),
+          { header: true, align: this.token.align[i] }
+        );
+      }
+      header += this.renderer.tablerow(cell);
+
+      for (i = 0; i < this.token.cells.length; i++) {
+        row = this.token.cells[i];
+
+        cell = '';
+        for (j = 0; j < row.length; j++) {
+          cell += this.renderer.tablecell(
+            this.inline.output(row[j]),
+            { header: false, align: this.token.align[j] }
+          );
+        }
+
+        body += this.renderer.tablerow(cell);
+      }
+      return this.renderer.table(header, body);
+    }
+    case 'blockquote_start': {
+      body = '';
+
+      while (this.next().type !== 'blockquote_end') {
+        body += this.tok();
+      }
+
+      return this.renderer.blockquote(body);
+    }
+    case 'list_start': {
+      body = '';
+      var ordered = this.token.ordered,
+          start = this.token.start;
+
+      while (this.next().type !== 'list_end') {
+        body += this.tok();
+      }
+
+      return this.renderer.list(body, ordered, start);
+    }
+    case 'list_item_start': {
+      body = '';
+      var loose = this.token.loose;
+
+      if (this.token.task) {
+        body += this.renderer.checkbox(this.token.checked);
+      }
+
+      while (this.next().type !== 'list_item_end') {
+        body += !loose && this.token.type === 'text'
+          ? this.parseText()
+          : this.tok();
+      }
+
+      return this.renderer.listitem(body);
+    }
+    case 'html': {
+      // TODO parse inline content if parameter markdown=1
+      return this.renderer.html(this.token.text);
+    }
+    case 'paragraph': {
+      return this.renderer.paragraph(this.inline.output(this.token.text));
+    }
+    case 'text': {
+      return this.renderer.paragraph(this.parseText());
+    }
+  }
+};
+
+/**
+ * Helpers
+ */
+
+function escape(html, encode) {
+  return html
+    .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+function unescape(html) {
+  // explicitly match decimal, hex, and named HTML entities
+  return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) {
+    n = n.toLowerCase();
+    if (n === 'colon') return ':';
+    if (n.charAt(0) === '#') {
+      return n.charAt(1) === 'x'
+        ? String.fromCharCode(parseInt(n.substring(2), 16))
+        : String.fromCharCode(+n.substring(1));
+    }
+    return '';
+  });
+}
+
+function edit(regex, opt) {
+  regex = regex.source || regex;
+  opt = opt || '';
+  return {
+    replace: function(name, val) {
+      val = val.source || val;
+      val = val.replace(/(^|[^\[])\^/g, '$1');
+      regex = regex.replace(name, val);
+      return this;
+    },
+    getRegex: function() {
+      return new RegExp(regex, opt);
+    }
+  };
+}
+
+function resolveUrl(base, href) {
+  if (!baseUrls[' ' + base]) {
+    // we can ignore everything in base after the last slash of its path component,
+    // but we might need to add _that_
+    // https://tools.ietf.org/html/rfc3986#section-3
+    if (/^[^:]+:\/*[^/]*$/.test(base)) {
+      baseUrls[' ' + base] = base + '/';
+    } else {
+      baseUrls[' ' + base] = rtrim(base, '/', true);
+    }
+  }
+  base = baseUrls[' ' + base];
+
+  if (href.slice(0, 2) === '//') {
+    return base.replace(/:[\s\S]*/, ':') + href;
+  } else if (href.charAt(0) === '/') {
+    return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href;
+  } else {
+    return base + href;
+  }
+}
+var baseUrls = {};
+var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
+
+function noop() {}
+noop.exec = noop;
+
+function merge(obj) {
+  var i = 1,
+      target,
+      key;
+
+  for (; i < arguments.length; i++) {
+    target = arguments[i];
+    for (key in target) {
+      if (Object.prototype.hasOwnProperty.call(target, key)) {
+        obj[key] = target[key];
+      }
+    }
+  }
+
+  return obj;
+}
+
+function splitCells(tableRow, count) {
+  // ensure that every cell-delimiting pipe has a space
+  // before it to distinguish it from an escaped pipe
+  var row = tableRow.replace(/\|/g, function (match, offset, str) {
+        var escaped = false,
+            curr = offset;
+        while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
+        if (escaped) {
+          // odd number of slashes means | is escaped
+          // so we leave it alone
+          return '|';
+        } else {
+          // add space before unescaped |
+          return ' |';
+        }
+      }),
+      cells = row.split(/ \|/),
+      i = 0;
+
+  if (cells.length > count) {
+    cells.splice(count);
+  } else {
+    while (cells.length < count) cells.push('');
+  }
+
+  for (; i < cells.length; i++) {
+    // leading or trailing whitespace is ignored per the gfm spec
+    cells[i] = cells[i].trim().replace(/\\\|/g, '|');
+  }
+  return cells;
+}
+
+// Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
+// /c*$/ is vulnerable to REDOS.
+// invert: Remove suffix of non-c chars instead. Default falsey.
+function rtrim(str, c, invert) {
+  if (str.length === 0) {
+    return '';
+  }
+
+  // Length of suffix matching the invert condition.
+  var suffLen = 0;
+
+  // Step left until we fail to match the invert condition.
+  while (suffLen < str.length) {
+    var currChar = str.charAt(str.length - suffLen - 1);
+    if (currChar === c && !invert) {
+      suffLen++;
+    } else if (currChar !== c && invert) {
+      suffLen++;
+    } else {
+      break;
+    }
+  }
+
+  return str.substr(0, str.length - suffLen);
+}
+
+/**
+ * Marked
+ */
+
+function marked(src, opt, callback) {
+  // throw error in case of non string input
+  if (typeof src === 'undefined' || src === null) {
+    throw new Error('marked(): input parameter is undefined or null');
+  }
+  if (typeof src !== 'string') {
+    throw new Error('marked(): input parameter is of type '
+      + Object.prototype.toString.call(src) + ', string expected');
+  }
+
+  if (callback || typeof opt === 'function') {
+    if (!callback) {
+      callback = opt;
+      opt = null;
+    }
+
+    opt = merge({}, marked.defaults, opt || {});
+
+    var highlight = opt.highlight,
+        tokens,
+        pending,
+        i = 0;
+
+    try {
+      tokens = Lexer.lex(src, opt)
+    } catch (e) {
+      return callback(e);
+    }
+
+    pending = tokens.length;
+
+    var done = function(err) {
+      if (err) {
+        opt.highlight = highlight;
+        return callback(err);
+      }
+
+      var out;
+
+      try {
+        out = Parser.parse(tokens, opt);
+      } catch (e) {
+        err = e;
+      }
+
+      opt.highlight = highlight;
+
+      return err
+        ? callback(err)
+        : callback(null, out);
+    };
+
+    if (!highlight || highlight.length < 3) {
+      return done();
+    }
+
+    delete opt.highlight;
+
+    if (!pending) return done();
+
+    for (; i < tokens.length; i++) {
+      (function(token) {
+        if (token.type !== 'code') {
+          return --pending || done();
+        }
+        return highlight(token.text, token.lang, function(err, code) {
+          if (err) return done(err);
+          if (code == null || code === token.text) {
+            return --pending || done();
+          }
+          token.text = code;
+          token.escaped = true;
+          --pending || done();
+        });
+      })(tokens[i]);
+    }
+
+    return;
+  }
+  try {
+    if (opt) opt = merge({}, marked.defaults, opt);
+    return Parser.parse(Lexer.lex(src, opt), opt);
+  } catch (e) {
+    e.message += '\nPlease report this to https://github.com/markedjs/marked.';
+    if ((opt || marked.defaults).silent) {
+      return '<p>An error occurred:</p><pre>'
+        + escape(e.message + '', true)
+        + '</pre>';
+    }
+    throw e;
+  }
+}
+
+/**
+ * Options
+ */
+
+marked.options =
+marked.setOptions = function(opt) {
+  merge(marked.defaults, opt);
+  return marked;
+};
+
+marked.getDefaults = function () {
+  return {
+    baseUrl: null,
+    breaks: false,
+    gfm: true,
+    headerIds: true,
+    headerPrefix: '',
+    highlight: null,
+    langPrefix: 'language-',
+    mangle: true,
+    pedantic: false,
+    renderer: new Renderer(),
+    sanitize: false,
+    sanitizer: null,
+    silent: false,
+    smartLists: false,
+    smartypants: false,
+    tables: true,
+    xhtml: false
+  };
+}
+
+marked.defaults = marked.getDefaults();
+
+/**
+ * Expose
+ */
+
+marked.Parser = Parser;
+marked.parser = Parser.parse;
+
+marked.Renderer = Renderer;
+marked.TextRenderer = TextRenderer;
+
+marked.Lexer = Lexer;
+marked.lexer = Lexer.lex;
+
+marked.InlineLexer = InlineLexer;
+marked.inlineLexer = InlineLexer.output;
+
+marked.parse = marked;
+
+if (typeof module !== 'undefined' && typeof exports === 'object') {
+  module.exports = marked;
+} else if (typeof define === 'function' && define.amd) {
+  define(function() { return marked; });
+} else {
+  root.marked = marked;
+}
+})(this || (typeof window !== 'undefined' ? window : global));

+ 18 - 0
components/uni/uParse/src/components/wxParseAudio.vue

@@ -0,0 +1,18 @@
+<template>
+	<audio :id="node.attr.id" :class="node.classStr" :style="node.styleStr" :src="node.attr.src" :loop="node.attr.loop"
+	 :poster="node.attr.poster" :name="node.attr.name" :author="node.attr.author" controls></audio>
+</template>
+
+<script>
+	export default {
+		name: 'wxParseAudio',
+		props: {
+			node: {
+				type: Object,
+				default () {
+					return {};
+				},
+			},
+		},
+	};
+</script>

+ 94 - 0
components/uni/uParse/src/components/wxParseImg.vue

@@ -0,0 +1,94 @@
+<template>
+	<image
+		:mode="node.attr.mode"
+		:lazy-load="node.attr.lazyLoad"
+		:class="node.classStr"
+		:style="newStyleStr || node.styleStr"
+		:data-src="node.attr.src"
+		:src="node.attr.src"
+		@tap="wxParseImgTap"
+		@load="wxParseImgLoad"
+	/>
+</template>
+
+<script>
+export default {
+	name: 'wxParseImg',
+	data() {
+		return {
+			newStyleStr: '',
+			preview: true
+		};
+	},
+	inject: ['parseWidth'],
+	mounted() {},
+	props: {
+		node: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	
+	methods: {
+		wxParseImgTap(e) {
+			if (!this.preview) return;
+			const { src } = e.currentTarget.dataset;
+			if (!src) return;
+			let parent = this.$parent;
+			while (!parent.preview || typeof parent.preview !== 'function') {
+				// TODO 遍历获取父节点执行方法
+				parent = parent.$parent;
+			}
+			parent.preview(src, e);
+		},
+		// 图片视觉宽高计算函数区
+		wxParseImgLoad(e) {
+			const { src } = e.currentTarget.dataset;
+			if (!src) return;
+			let { width, height } = e.mp.detail;
+
+			const recal = this.wxAutoImageCal(width, height);
+
+			const { imageheight, imageWidth } = recal;
+			const { padding, mode } = this.node.attr;//删除padding
+			// const { mode } = this.node.attr;
+
+			const { styleStr } = this.node;
+			const imageHeightStyle = mode === 'widthFix' ? '' : `height: ${imageheight}px;`;
+
+			this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px; padding: 0 ${+padding}px;`;//删除padding
+			// this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px;`;
+		},
+		// 计算视觉优先的图片宽高
+		wxAutoImageCal(originalWidth, originalHeight) {
+			// 获取图片的原始长宽
+			const windowWidth = this.parseWidth.value;
+			const results = {};
+
+			if (originalWidth < 60 || originalHeight < 60) {
+				const { src } = this.node.attr;
+				let parent = this.$parent;
+				while (!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.removeImageUrl(src);
+				this.preview = false;
+			}
+
+			// 判断按照那种方式进行缩放
+			if (originalWidth > windowWidth) {
+				// 在图片width大于手机屏幕width时候
+				results.imageWidth = windowWidth;
+				results.imageheight = windowWidth * (originalHeight / originalWidth);
+			} else {
+				// 否则展示原来的数据
+				results.imageWidth = originalWidth;
+				results.imageheight = originalHeight;
+			}
+			return results;
+		}
+	}
+};
+</script>

+ 63 - 0
components/uni/uParse/src/components/wxParseTable.vue

@@ -0,0 +1,63 @@
+<template>
+	<view class="wxTable" @tap="wxParseTableTap">
+		<rich-text :nodes="nodes" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+	</view>
+</template>
+<script>
+export default {
+	name: 'wxParseTable',
+	props: {
+		node: {
+			type: Object,
+			default() {
+				return {};
+			},
+		},
+	},
+	inject: ['parseSelect'],
+	data() {
+		return {
+			nodes:[]
+		};
+	},
+	mounted() {
+		this.nodes=this.loadNode([this.node]);
+	},
+	methods: {
+		wxParseTableTap(e) {
+			let parent = this.$parent;
+			while (!parent.preview || typeof parent.preview !== 'function') {
+				// TODO 遍历获取父节点执行方法
+				parent = parent.$parent;
+			}
+			parent.preview("", e);
+		},
+		loadNode(node) {
+			let obj = [];
+			for (let children of node) {
+				if (children.node=='element') {
+					let t = {
+						name:children.tag,
+						attrs: {
+							class: children.classStr
+						},
+						children: children.nodes?this.loadNode(children.nodes):[]
+					}
+					if(children.tag==="img"){
+						t.attrs.src= children.attr.src;
+						t.attrs.mode=children.attr.mode;
+						t.attrs.alt=children.attr.alt;
+					}
+					obj.push(t)
+				} else if(children.node=='text'){
+					obj.push({
+						type: 'text',
+						text: children.text
+					})
+				}
+			}
+			return obj
+		}
+	}
+};
+</script>

+ 94 - 0
components/uni/uParse/src/components/wxParseTemplate0.vue

@@ -0,0 +1,94 @@
+<template>
+	<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node"/>
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'"><text decode="true">{{(node.text || "").replace(/\n/g, '\n')}}</text></block>
+	<!-- <block v-else-if="node.node == 'text'">{{node.text}}</block> -->
+</template>
+
+<script>
+	// #ifdef APP-PLUS | H5
+	import wxParseTemplate from './wxParseTemplate0';
+	// #endif
+	// #ifdef MP
+	import wxParseTemplate from './wxParseTemplate1';
+	// #endif
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+
+	export default {
+		name: 'wxParseTemplate',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;// TODO currentTarget才有dataset
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {// TODO 遍历获取父节点执行方法
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate1.vue

@@ -0,0 +1,88 @@
+<template>
+	<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate2';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate1',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate10.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate11';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate10',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 86 - 0
components/uni/uParse/src/components/wxParseTemplate11.vue

@@ -0,0 +1,86 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate11',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate2.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate3';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate2',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate3.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate4';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate3',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate4.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate5';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate4',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate5.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate6';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate5',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 89 - 0
components/uni/uParse/src/components/wxParseTemplate6.vue

@@ -0,0 +1,89 @@
+<template>
+	<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href"
+		 :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+		<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+		<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'" />
+
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'" />
+
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr" />
+
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate7';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+
+	export default {
+		name: 'wxParseTemplate6',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr, e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while (!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate7.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate8';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate7',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate8.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate9';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate8',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uni/uParse/src/components/wxParseTemplate9.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate10';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate9',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

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