u-tabbar.vue 9.4 KB

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