u-tabbar.vue 10 KB

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