1
0

u-tabbar.vue 9.4 KB

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