guangchao.xu | 070005a | 2020-12-07 09:56:40 +0800 | [diff] [blame] | 1 | <template> |
| 2 | <view v-if="show" class="u-tabbar" @touchmove.stop.prevent="() => {}"> |
| 3 | <view class="u-tabbar__content safe-area-inset-bottom" :style="{ |
| 4 | height: $u.addUnit(height), |
| 5 | backgroundColor: bgColor, |
| 6 | }" :class="{ |
| 7 | 'u-border-top': borderTop |
| 8 | }"> |
| 9 | <view class="u-tabbar__content__item" v-for="(item, index) in list" :key="index" :class="{ |
| 10 | 'u-tabbar__content__circle': midButton &&item.midButton |
| 11 | }" @tap.stop="clickHandler(index)" :style="{ |
| 12 | backgroundColor: bgColor |
| 13 | }"> |
| 14 | <view :class="[ |
| 15 | midButton && item.midButton ? 'u-tabbar__content__circle__button' : 'u-tabbar__content__item__button' |
| 16 | ]"> |
| 17 | <u-icon |
| 18 | :size="midButton && item.midButton ? midButtonSize : iconSize" |
| 19 | :name="elIconPath(index)" |
| 20 | img-mode="scaleToFill" |
| 21 | :color="elColor(index)" |
| 22 | :custom-prefix="item.customIcon ? 'custom-icon' : 'uicon'" |
| 23 | ></u-icon> |
| 24 | <u-badge :count="item.count" :is-dot="item.isDot" |
| 25 | v-if="item.count" |
| 26 | :offset="[-2, getOffsetRight(item.count, item.isDot)]" |
| 27 | ></u-badge> |
| 28 | </view> |
| 29 | <view class="u-tabbar__content__item__text" :style="{ |
| 30 | color: elColor(index) |
| 31 | }"> |
| 32 | <text class="u-line-1">{{item.text}}</text> |
| 33 | </view> |
| 34 | </view> |
| 35 | <view v-if="midButton" class="u-tabbar__content__circle__border" :class="{ |
| 36 | 'u-border': borderTop, |
| 37 | }" :style="{ |
| 38 | backgroundColor: bgColor, |
| 39 | left: midButtonLeft |
| 40 | }"> |
| 41 | </view> |
| 42 | </view> |
| 43 | <!-- 这里加上一个48rpx的高度,是为了增高有凸起按钮时的防塌陷高度(也即按钮凸出来部分的高度) --> |
| 44 | <view class="u-fixed-placeholder safe-area-inset-bottom" :style="{ |
| 45 | height: `calc(${$u.addUnit(height)} + ${midButton ? 48 : 0}rpx)`, |
| 46 | }"></view> |
| 47 | </view> |
| 48 | </template> |
| 49 | |
| 50 | <script> |
| 51 | export default { |
| 52 | props: { |
| 53 | // 显示与否 |
| 54 | show: { |
| 55 | type: Boolean, |
| 56 | default: true |
| 57 | }, |
| 58 | // 通过v-model绑定current值 |
| 59 | value: { |
| 60 | type: [String, Number], |
| 61 | default: 0 |
| 62 | }, |
| 63 | // 整个tabbar的背景颜色 |
| 64 | bgColor: { |
| 65 | type: String, |
| 66 | default: '#ffffff' |
| 67 | }, |
| 68 | // tabbar的高度,默认50px,单位任意,如果为数值,则为rpx单位 |
| 69 | height: { |
| 70 | type: [String, Number], |
| 71 | default: '50px' |
| 72 | }, |
| 73 | // 非凸起图标的大小,单位任意,数值默认rpx |
| 74 | iconSize: { |
| 75 | type: [String, Number], |
| 76 | default: 40 |
| 77 | }, |
| 78 | // 凸起的图标的大小,单位任意,数值默认rpx |
| 79 | midButtonSize: { |
| 80 | type: [String, Number], |
| 81 | default: 90 |
| 82 | }, |
| 83 | // 激活时的演示,包括字体图标,提示文字等的演示 |
| 84 | activeColor: { |
| 85 | type: String, |
| 86 | default: '#303133' |
| 87 | }, |
| 88 | // 未激活时的颜色 |
| 89 | inactiveColor: { |
| 90 | type: String, |
| 91 | default: '#606266' |
| 92 | }, |
| 93 | // 是否显示中部的凸起按钮 |
| 94 | midButton: { |
| 95 | type: Boolean, |
| 96 | default: false |
| 97 | }, |
| 98 | // 配置参数 |
| 99 | list: { |
| 100 | type: Array, |
| 101 | default () { |
| 102 | return [] |
| 103 | } |
| 104 | }, |
| 105 | // 切换前的回调 |
| 106 | beforeSwitch: { |
| 107 | type: Function, |
| 108 | default: null |
| 109 | }, |
| 110 | // 是否显示顶部的横线 |
| 111 | borderTop: { |
| 112 | type: Boolean, |
| 113 | default: true |
| 114 | }, |
| 115 | // 是否隐藏原生tabbar |
| 116 | hideTabBar: { |
| 117 | type: Boolean, |
| 118 | default: true |
| 119 | }, |
| 120 | }, |
| 121 | data() { |
| 122 | return { |
| 123 | // 由于安卓太菜了,通过css居中凸起按钮的外层元素有误差,故通过js计算将其居中 |
| 124 | midButtonLeft: '50%', |
| 125 | pageUrl: '', // 当前页面URL |
| 126 | } |
| 127 | }, |
| 128 | created() { |
| 129 | // 是否隐藏原生tabbar |
| 130 | if(this.hideTabBar) uni.hideTabBar(); |
| 131 | // 获取引入了u-tabbar页面的路由地址,该地址没有路径前面的"/" |
| 132 | let pages = getCurrentPages(); |
| 133 | // 页面栈中的最后一个即为项为当前页面,route属性为页面路径 |
| 134 | this.pageUrl = pages[pages.length - 1].route; |
| 135 | }, |
| 136 | computed: { |
| 137 | elIconPath() { |
| 138 | return (index) => { |
| 139 | // 历遍u-tabbar的每一项item时,判断是否传入了pagePath参数,如果传入了 |
| 140 | // 和data中的pageUrl参数对比,如果相等,即可判断当前的item对应当前的tabbar页面,设置高亮图标 |
| 141 | // 采用这个方法,可以无需使用v-model绑定的value值 |
| 142 | let pagePath = this.list[index].pagePath; |
| 143 | // 如果定义了pagePath属性,意味着使用系统自带tabbar方案,否则使用一个页面用几个组件模拟tabbar页面的方案 |
| 144 | // 这两个方案对处理tabbar item的激活与否方式不一样 |
| 145 | if(pagePath) { |
| 146 | if(pagePath == this.pageUrl || pagePath == '/' + this.pageUrl) { |
| 147 | return this.list[index].selectedIconPath; |
| 148 | } else { |
| 149 | return this.list[index].iconPath; |
| 150 | } |
| 151 | } else { |
| 152 | // 普通方案中,索引等于v-model值时,即为激活项 |
| 153 | return index == this.value ? this.list[index].selectedIconPath : this.list[index].iconPath |
| 154 | } |
| 155 | } |
| 156 | }, |
| 157 | elColor() { |
| 158 | return (index) => { |
| 159 | // 判断方法同理于elIconPath |
| 160 | let pagePath = this.list[index].pagePath; |
| 161 | if(pagePath) { |
| 162 | if(pagePath == this.pageUrl || pagePath == '/' + this.pageUrl) return this.activeColor; |
| 163 | else return this.inactiveColor; |
| 164 | } else { |
| 165 | return index == this.value ? this.activeColor : this.inactiveColor; |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | }, |
| 170 | mounted() { |
| 171 | this.midButton && this.getMidButtonLeft(); |
| 172 | }, |
| 173 | methods: { |
| 174 | async clickHandler(index) { |
| 175 | if(this.beforeSwitch && typeof(this.beforeSwitch) === 'function') { |
| 176 | // 执行回调,同时传入索引当作参数 |
| 177 | // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this |
| 178 | // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文 |
| 179 | let beforeSwitch = this.beforeSwitch.bind(this.$u.$parent.call(this))(index); |
| 180 | // 判断是否返回了promise |
| 181 | if (!!beforeSwitch && typeof beforeSwitch.then === 'function') { |
| 182 | await beforeSwitch.then(res => { |
| 183 | // promise返回成功, |
| 184 | this.switchTab(index); |
| 185 | }).catch(err => { |
| 186 | |
| 187 | }) |
| 188 | } else if(beforeSwitch === true) { |
| 189 | // 如果返回true |
| 190 | this.switchTab(index); |
| 191 | } |
| 192 | } else { |
| 193 | this.switchTab(index); |
| 194 | } |
| 195 | }, |
| 196 | // 切换tab |
| 197 | switchTab(index) { |
| 198 | // 发出事件和修改v-model绑定的值 |
| 199 | this.$emit('change', index); |
| 200 | // 如果有配置pagePath属性,使用uni.switchTab进行跳转 |
| 201 | if(this.list[index].pagePath) { |
| 202 | uni.switchTab({ |
| 203 | url: this.list[index].pagePath |
| 204 | }) |
| 205 | } else { |
| 206 | // 如果配置了papgePath属性,将不会双向绑定v-model传入的value值 |
| 207 | // 因为这个模式下,不再需要v-model绑定的value值了,而是通过getCurrentPages()适配 |
| 208 | this.$emit('input', index); |
| 209 | } |
| 210 | }, |
| 211 | // 计算角标的right值 |
| 212 | getOffsetRight(count, isDot) { |
| 213 | // 点类型,count大于9(两位数),分别设置不同的right值,避免位置太挤 |
| 214 | if(isDot) { |
| 215 | return -20; |
| 216 | } else if(count > 9) { |
| 217 | return -40; |
| 218 | } else { |
| 219 | return -30; |
| 220 | } |
| 221 | }, |
| 222 | // 获取凸起按钮外层元素的left值,让其水平居中 |
| 223 | getMidButtonLeft() { |
| 224 | let windowWidth = this.$u.sys().windowWidth; |
| 225 | // 由于安卓中css计算left: 50%的结果不准确,故用js计算 |
| 226 | this.midButtonLeft = (windowWidth / 2) + 'px'; |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | </script> |
| 231 | |
| 232 | <style scoped lang="scss"> |
| 233 | @import "../../libs/css/style.components.scss"; |
| 234 | .u-fixed-placeholder { |
| 235 | /* #ifndef APP-NVUE */ |
| 236 | box-sizing: content-box; |
| 237 | /* #endif */ |
| 238 | } |
| 239 | |
| 240 | .u-tabbar { |
| 241 | |
| 242 | &__content { |
| 243 | @include vue-flex; |
| 244 | align-items: center; |
| 245 | position: relative; |
| 246 | position: fixed; |
| 247 | bottom: 0; |
| 248 | left: 0; |
| 249 | width: 100%; |
| 250 | z-index: 998; |
| 251 | /* #ifndef APP-NVUE */ |
| 252 | box-sizing: content-box; |
| 253 | /* #endif */ |
| 254 | |
| 255 | &__circle__border { |
| 256 | border-radius: 100%; |
| 257 | width: 110rpx; |
| 258 | height: 110rpx; |
| 259 | top: -48rpx; |
| 260 | position: absolute; |
| 261 | z-index: 4; |
| 262 | background-color: #ffffff; |
| 263 | // 由于安卓的无能,导致只有3个tabbar item时,此css计算方式有误差 |
| 264 | // 故使用js计算的形式来定位,此处不注释,是因为js计算有延后,避免出现位置闪动 |
| 265 | left: 50%; |
| 266 | transform: translateX(-50%); |
| 267 | |
| 268 | &:after { |
| 269 | border-radius: 100px; |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | &__item { |
| 274 | flex: 1; |
| 275 | justify-content: center; |
| 276 | height: 100%; |
| 277 | padding: 12rpx 0; |
| 278 | @include vue-flex; |
| 279 | flex-direction: column; |
| 280 | align-items: center; |
| 281 | position: relative; |
| 282 | |
| 283 | &__button { |
| 284 | position: absolute; |
| 285 | top: 14rpx; |
| 286 | left: 50%; |
| 287 | transform: translateX(-50%); |
| 288 | } |
| 289 | |
| 290 | &__text { |
| 291 | color: $u-content-color; |
| 292 | font-size: 26rpx; |
| 293 | line-height: 28rpx; |
| 294 | position: absolute; |
| 295 | bottom: 14rpx; |
| 296 | left: 50%; |
| 297 | transform: translateX(-50%); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | &__circle { |
| 302 | position: relative; |
| 303 | @include vue-flex; |
| 304 | flex-direction: column; |
| 305 | justify-content: space-between; |
| 306 | z-index: 10; |
| 307 | /* #ifndef APP-NVUE */ |
| 308 | height: calc(100% - 1px); |
| 309 | /* #endif */ |
| 310 | |
| 311 | &__button { |
| 312 | width: 90rpx; |
| 313 | height: 90rpx; |
| 314 | border-radius: 100%; |
| 315 | @include vue-flex; |
| 316 | justify-content: center; |
| 317 | align-items: center; |
| 318 | position: absolute; |
| 319 | background-color: #ffffff; |
| 320 | top: -40rpx; |
| 321 | left: 50%; |
| 322 | z-index: 6; |
| 323 | transform: translateX(-50%); |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | </style> |