| <template> | 
|   <view v-if="show" class="tn-tabbar-class tn-tabbar" @touchmove.stop.prevent="() => {}"> | 
|     <!-- tabbar 内容--> | 
|     <view | 
|       class="tn-tabbar__content" | 
|       :class="{ | 
|         'tn-tabbar--fixed': fixed, | 
|         'tn-safe-area-inset-bottom': safeAreaInsetBottom, | 
|         'tn-tabbar--shadow': shadow | 
|       }" | 
|       :style="{ | 
|         height: height + 'rpx', | 
|         backgroundColor: bgColor | 
|       }" | 
|     > | 
|       <!-- tabbar item --> | 
|       <view | 
|         v-for="(item, index) in list" | 
|         :key="index" | 
|         class="tn-tabbar__content__item" | 
|         :id="`tabbar_item_${index}`" | 
|         :class="{'tn-tabbar__content__item--out': item.out}" | 
|         :style="{ | 
|           backgroundColor: bgColor | 
|         }" | 
|         @tap.stop="clickItemHandler(index)" | 
|       > | 
|         <!-- tabbar item的图片或者icon--> | 
|         <view :class="[itemButtonClass(index)]" | 
|           :style="[itemButtonStyle(index)]" | 
|         > | 
|           <image | 
|             v-if="isImage(index)" | 
|             :src="elIcon(index)" | 
|             mode="scaleToFill" | 
|             class="tn-tabbar__content__item__image" | 
|             :style="{ | 
|               width: `${item.iconSize || iconSize}rpx`, | 
|               height: `${item.iconSize || iconSize}rpx` | 
|             }" | 
|           ></image> | 
|           <view | 
|             v-else | 
|             class="tn-tabbar__content__item__icon" | 
|             :class="[`tn-icon-${elIcon(index)}`,elIconColor(index, false)]" | 
|             :style="{ | 
|               fontSize: `${item.iconSize || iconSize}rpx`, | 
|               color: elIconColor(index) | 
|             }" | 
|           ></view> | 
|            | 
|           <!-- 角标--> | 
|           <tn-badge | 
|             v-if="!item.out && (item.count || item.dot)" | 
|             :dot="item.dot || false" | 
|             backgroundColor="tn-bg-red" | 
|             fontColor="#FFFFFF" | 
|             :radius="item.dot ? 14 : 0" | 
|             :fontSize="14" | 
|             padding="2rpx 4rpx" | 
|             :absolute="true" | 
|             :top="2" | 
|           > | 
|             {{ $t.number.formatNumberString(item.count) }} | 
|           </tn-badge> | 
|         </view> | 
|          | 
|         <!-- tabbar item的文字--> | 
|         <view | 
|           class="tn-tabbar__content__item__text" | 
|           :class="[elColor(index, false)]" | 
|           :style="{ | 
|             color: elColor(index), | 
|             fontSize: `${fontSize}rpx` | 
|           }" | 
|         > | 
|           <text class="tn-text-ellipsis">{{ item.title }}</text> | 
|         </view> | 
|       </view> | 
|        | 
|       <!-- item 突起部分 --> | 
|       <view | 
|         v-if="outItemIndex !== -1" | 
|         class="tn-tabbar__content__out" | 
|         :class="[{ | 
|           'tn-tabbar__content__out--shadow': shadow | 
|         }, animation && value === outItemIndex ? `tn-tabbar__content__out--animation--${animationMode}` : '']" | 
|         :style="{ | 
|           backgroundColor: bgColor, | 
|           left: outItemLeft, | 
|           width: `${outHeight}rpx`, | 
|           height: `${outHeight}rpx`, | 
|           top: `-${outHeight * 0.3}rpx` | 
|         }" | 
|         @tap.stop="clickItemHandler(outItemIndex)" | 
|       ></view> | 
|     </view> | 
|      | 
|     <!-- 防止tabbar塌陷 --> | 
|     <view class="tn-tabbar__placeholder" :class="{'tn-safe-area-inset-bottom': safeAreaInsetBottom}" :style="{ | 
|       height: `calc(${height}rpx)` | 
|     }"></view> | 
|   </view> | 
| </template> | 
|   | 
| <script> | 
|   export default { | 
|     name: 'tn-tabbar', | 
|     props: { | 
|       // 绑定当前被选中的current值 | 
|       value: { | 
|         type: [String, Number], | 
|         default: 0 | 
|       }, | 
|       // 是否显示 | 
|       show: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       // 图标列表 | 
|       list: { | 
|         type: Array, | 
|         default() { | 
|           return [] | 
|         } | 
|       }, | 
|       // 高度,单位rpx | 
|       height: { | 
|         type: Number, | 
|         default: 100 | 
|       }, | 
|       // 突起的高度 | 
|       outHeight: { | 
|         type: Number, | 
|         default: 100 | 
|       }, | 
|       // 背景颜色 | 
|       bgColor: { | 
|         type: String, | 
|         default: '#FFFFFF' | 
|       }, | 
|       // 图标大小 | 
|       iconSize: { | 
|         type: Number, | 
|         default: 50 | 
|       }, | 
|       // 字体大小 | 
|       fontSize: { | 
|         type: Number, | 
|         default: 20 | 
|       }, | 
|       // 激活时的颜色 | 
|       activeColor: { | 
|         type: String, | 
|         default: '#01BEFF' | 
|       }, | 
|       // 非激活时的颜色 | 
|       inactiveColor: { | 
|         type: String, | 
|         default: '#AAAAAA' | 
|       }, | 
|       // 激活时图标的颜色 | 
|       activeIconColor: { | 
|         type: String, | 
|         default: '#01BEFF' | 
|       }, | 
|       // 非激活时图标的颜色 | 
|       inactiveIconColor: { | 
|         type: String, | 
|         default: '#AAAAAA' | 
|       }, | 
|       // 激活时的自定义样式 | 
|       activeStyle: { | 
|         type: Object, | 
|         default() { | 
|           return {} | 
|         } | 
|       }, | 
|       // 是否显示阴影 | 
|       shadow: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       // 点击时是否有动画 | 
|       animation: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       // 点击时的动画模式 | 
|       animationMode: { | 
|         type: String, | 
|         default: 'scale' | 
|       }, | 
|       // 是否固定在底部 | 
|       fixed: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 | 
|       safeAreaInsetBottom: { | 
|           type: Boolean, | 
|           default: false | 
|       }, | 
|       // 切换前回调 | 
|       beforeSwitch: { | 
|         type: Function, | 
|         default: null | 
|       } | 
|     }, | 
|     computed: { | 
|       // 当前字体的颜色 | 
|       elColor() { | 
|         return (index, style = true) => { | 
|           let currentItem = this.list[index] | 
|           let color = '' | 
|           if (index === this.value) { | 
|             color = currentItem['activeColor'] || this.activeColor | 
|           } else { | 
|             color = currentItem['inactiveColor'] || this.inactiveColor | 
|           } | 
|           // 判断是否获取内部样式 | 
|           if (style) { | 
|             if (this.$t.color.getFontColorStyle(color) !== '') { | 
|               return color | 
|             } else { | 
|               return '' | 
|             } | 
|           } else { | 
|             if (this.$t.color.getFontColorStyle(color) === '') { | 
|               return color | 
|             } else { | 
|               return '' | 
|             } | 
|           } | 
|         } | 
|       }, | 
|       // 当前图标的颜色 | 
|       elIconColor() { | 
|         return (index, style = true) => { | 
|           let currentItem = this.list[index] | 
|           let color = '' | 
|           if (index === this.value) { | 
|             color = currentItem['activeIconColor'] || this.activeIconColor | 
|           } else { | 
|             color = currentItem['inactiveIconColor'] || this.inactiveIconColor | 
|           } | 
|           // 判断是否获取内部样式 | 
|           if (style) { | 
|             if (this.$t.color.getFontColorStyle(color) !== '') { | 
|               return color | 
|             } else { | 
|               return '' | 
|             } | 
|           } else { | 
|             if (this.$t.color.getFontColorStyle(color) === '') { | 
|               return color + ' tn-tabbar__content__item__icon--clip' | 
|             } else { | 
|               return '' | 
|             } | 
|           } | 
|         } | 
|       }, | 
|       // 当前的图标 | 
|       elIcon() { | 
|         return (index) => { | 
|           let currentItem = this.list[index] | 
|           if (index === this.value) { | 
|             return currentItem['activeIcon'] | 
|           } else { | 
|             return currentItem['inactiveIcon'] | 
|           } | 
|         } | 
|       }, | 
|       // 突起部分item button对应的类 | 
|       itemButtonClass() { | 
|         return (index) => { | 
|           let clazz = '' | 
|           if (this.list[index]['out']) { | 
|             clazz += 'tn-tabbar__content__item__button--out' | 
|             if (this.$t.color.getFontColorStyle(this.activeIconColor) === '') { | 
|               clazz += ` ${this.activeIconColor}` | 
|             } | 
|             if (this.value === index) { | 
|               clazz += ` tn-tabbar__content__item__button--out--animation--${this.animationMode}` | 
|             } | 
|           } else { | 
|             clazz += 'tn-tabbar__content__item__button' | 
|             if (this.value === index) { | 
|               clazz += ` tn-tabbar__content__item__button--animation--${this.animationMode}` | 
|             } | 
|           } | 
|           return clazz | 
|         } | 
|       }, | 
|       // 突起部分item button样式 | 
|       itemButtonStyle() { | 
|         return (index) => { | 
|           let style = {} | 
|           if (this.list[index]['out']) { | 
|             if (this.$t.color.getFontColorStyle(this.activeIconColor) !== '') { | 
|               style.backgroundColor = this.activeIconColor | 
|             } | 
|             style.width = `${this.outHeight - 35}rpx` | 
|             style.height = `${this.outHeight - 35}rpx` | 
|             style.top = `-${this.outHeight * 0.15}rpx` | 
|              | 
|             return style | 
|           } | 
|           return style | 
|         } | 
|       }, | 
|       // 判断图标是否为图片 | 
|       isImage() { | 
|         return (index) => { | 
|           const icon = this.list[index]['activeIcon'] | 
|           // 只有包含了'/'就认为是图片 | 
|           return icon.indexOf('/') !== -1 | 
|         } | 
|       } | 
|     }, | 
|     data() { | 
|       return { | 
|         // 当前突起的位置 | 
|         outItemLeft: '50%', | 
|         // 当前设置了突起按钮的index | 
|         outItemIndex: -1, | 
|         // 每一个item的信息 | 
|         tabbatItemInfo: [] | 
|       } | 
|     }, | 
|     watch: { | 
|        | 
|     }, | 
|     created() { | 
|       this.getOutItemIndex() | 
|     }, | 
|     mounted() { | 
|       this.$nextTick(() => { | 
|         this.getTabbarItem() | 
|       }) | 
|     }, | 
|     methods: { | 
|       // 获取每一个item的信息 | 
|       getTabbarItem() { | 
|         let query = uni.createSelectorQuery().in(this) | 
|         // 遍历获取信息 | 
|         for (let i = 0; i < this.list.length; i++) { | 
|           query.select(`#tabbar_item_${i}`).fields({ | 
|             size: true, | 
|             rect: true | 
|           }) | 
|         } | 
|         query.exec(res => { | 
|           if (!res) { | 
|             setTimeout(() => { | 
|               this.getTabbarItem() | 
|             }, 10) | 
|             return | 
|           } | 
|           this.tabbatItemInfo = res.map((item) => { | 
|             return { | 
|               left: item.left, | 
|               width: item.width | 
|             } | 
|           }) | 
|           this.updateOutItemLeft() | 
|         }) | 
|       }, | 
|       // 获取突起Item所在的index(如果存在) | 
|       getOutItemIndex() { | 
|         this.outItemIndex = this.list.findIndex((item) => { | 
|           return item.hasOwnProperty('out') && item.out | 
|         }) | 
|       }, | 
|       // 点击底部菜单时触发 | 
|       async clickItemHandler(index) { | 
|         if (this.beforeSwitch && typeof(this.beforeSwitch) === 'function') { | 
|           // 执行回调,同时传入索引当作参数 | 
|           // 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this | 
|           // 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文 | 
|           let beforeSwitch = this.beforeSwitch.bind(this.$t.$parent.call(this))(index) | 
|           // 判断是否返回了Promise | 
|           if (!!beforeSwitch && typeof beforeSwitch.then === 'function') { | 
|             await beforeSwitch.then(res => { | 
|               // Promise返回成功 | 
|               this.switchTab(index) | 
|             }).catch(err => { | 
|                | 
|             }) | 
|           } else if (beforeSwitch === true) { | 
|             this.switchTab(index) | 
|           } | 
|         } else { | 
|           this.switchTab(index) | 
|         } | 
|       }, | 
|       // 切换tab | 
|       switchTab(index) { | 
|         // 发出事件和修改v-model绑定的值 | 
|         this.$emit('change', index) | 
|         this.$emit('input', index) | 
|       }, | 
|       // 设置突起的位置 | 
|       updateOutItemLeft() { | 
|         // 查找出需要突起的元素 | 
|         const index = this.list.findIndex((item) => { | 
|           return item.out | 
|         }) | 
|         if (index !== -1) { | 
|           this.outItemLeft = this.tabbatItemInfo[index].left + (this.tabbatItemInfo[index].width / 2) + 'px' | 
|         } | 
|       } | 
|     } | 
|   } | 
| </script> | 
|   | 
| <style lang="scss" scoped> | 
|    | 
|   .tn-tabbar { | 
|      | 
|     &__content { | 
|       box-sizing: content-box; | 
|       display: flex; | 
|       flex-direction: row; | 
|       align-items: center; | 
|       position: relative; | 
|       width: 100%; | 
|       z-index: 1024; | 
|        | 
|       &__out { | 
|         position: absolute; | 
|         z-index: 4; | 
|         border-radius: 100%; | 
|         left: 50%; | 
|         transform: translateX(-50%); | 
|          | 
|         &--shadow { | 
|           box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05); | 
|            | 
|           &::before { | 
|             content: " "; | 
|             position: absolute; | 
|             width: 100%; | 
|             height: 50rpx; | 
|             bottom: 0; | 
|             left: 0; | 
|             right: 0; | 
|             margin: auto; | 
|             background-color: inherit; | 
|           } | 
|         } | 
|          | 
|         &--animation { | 
|           &--scale { | 
|             transform-origin: 50% 100%; | 
|             animation:tabbar-content-out-click 0.2s forwards 1 ease-in-out; | 
|           } | 
|         } | 
|       } | 
|        | 
|       &__item { | 
|         flex: 1; | 
|         display: flex; | 
|         flex-direction: column; | 
|         justify-content: flex-end; | 
|         align-items: center; | 
|         height: 100%; | 
|         position: relative; | 
|          | 
|         &__button { | 
|           margin-bottom: 10rpx; | 
|           display: flex; | 
|           align-items: center; | 
|           justify-content: center; | 
|           position: relative; | 
|            | 
|           &--out { | 
|             margin-bottom: 10rpx; | 
|             border-radius: 50%; | 
|             position: absolute; | 
|             display: flex; | 
|             justify-content: center; | 
|             align-items: center; | 
|             z-index: 6; | 
|              | 
|             &--animation { | 
|               &--scale { | 
|                 transform-origin: 50% 100%; | 
|                 animation:tabbar-item-button-out-click 0.2s forwards 1; | 
|               } | 
|             } | 
|           } | 
|            | 
|           &--animation { | 
|             &--scale { | 
|               .tn-tabbar__content__item__icon, .tn-tabbar__content__item__image { | 
|                 transform-origin: 50% 100%; | 
|                 animation:tabbar-item-button-click 0.2s forwards 1; | 
|               } | 
|             } | 
|           } | 
|         } | 
|          | 
|         &__icon { | 
|            | 
|           &--clip { | 
|             -webkit-background-clip: text; | 
|             color: transparent !important; | 
|           } | 
|         } | 
|          | 
|         &__text { | 
|           width: 100%; | 
|           font-size: 26rpx; | 
|           line-height: 28rpx; | 
|           text-align: center; | 
|           margin-bottom: 10rpx; | 
|           z-index: 10; | 
|           transition: all 0.2s ease-in-out; | 
|         } | 
|          | 
|         &--out { | 
|           height: calc(100% - 1px); | 
|         } | 
|       } | 
|     } | 
|      | 
|     &--fixed { | 
|       position: fixed; | 
|       bottom: 0; | 
|       left: 0; | 
|       right: 0; | 
|     } | 
|      | 
|     &--shadow { | 
|       box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07); | 
|     } | 
|   } | 
|    | 
|   /* 点击动画 start */ | 
|    | 
|   @keyframes tabbar-item-button-click{ | 
|     from{ | 
|       transform: scale(0.8); | 
|     } | 
|     to{ | 
|       transform: scale(1); | 
|     } | 
|   } | 
|    | 
|   @keyframes tabbar-item-button-out-click { | 
|     0%{ | 
|       transform: translateY(0) scale(1); | 
|     } | 
|     50%{ | 
|       transform: translateY(-10rpx) scale(1.2); | 
|     } | 
|     100%{ | 
|       transform: translateY(0) scale(1); | 
|     } | 
|   } | 
|    | 
|   @keyframes tabbar-content-out-click { | 
|     0%{ | 
|       transform: translateX(-50%) translateY(0) scale(1); | 
|     } | 
|     50% { | 
|       transform: translateX(-50%) translateY(-10rpx) scale(1.1); | 
|     } | 
|     100% { | 
|       transform: translateX(-50%) translateY(0) scale(1); | 
|     } | 
|   } | 
|    | 
|   /* 点击动画 end */ | 
| </style> |