tn-tabbar.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. <template>
  2. <view v-if="show" class="tn-tabbar-class tn-tabbar" @touchmove.stop.prevent="() => {}">
  3. <!-- tabbar 内容-->
  4. <view
  5. class="tn-tabbar__content"
  6. :class="{
  7. 'tn-tabbar--fixed': fixed,
  8. 'tn-safe-area-inset-bottom': safeAreaInsetBottom,
  9. 'tn-tabbar--shadow': shadow
  10. }"
  11. :style="{
  12. height: height + 'rpx',
  13. backgroundColor: bgColor
  14. }"
  15. >
  16. <!-- tabbar item -->
  17. <view
  18. v-for="(item, index) in list"
  19. :key="index"
  20. class="tn-tabbar__content__item"
  21. :id="`tabbar_item_${index}`"
  22. :class="{'tn-tabbar__content__item--out': item.out}"
  23. :style="{
  24. backgroundColor: bgColor
  25. }"
  26. @tap.stop="clickItemHandler(index)"
  27. >
  28. <!-- tabbar item的图片或者icon-->
  29. <view :class="[itemButtonClass(index)]"
  30. :style="[itemButtonStyle(index)]"
  31. >
  32. <image
  33. v-if="isImage(index)"
  34. :src="elIcon(index)"
  35. mode="scaleToFill"
  36. class="tn-tabbar__content__item__image"
  37. :style="{
  38. width: `${item.iconSize || iconSize}rpx`,
  39. height: `${item.iconSize || iconSize}rpx`
  40. }"
  41. ></image>
  42. <view
  43. v-else
  44. class="tn-tabbar__content__item__icon"
  45. :class="[`tn-icon-${elIcon(index)}`,elIconColor(index, false)]"
  46. :style="{
  47. fontSize: `${item.iconSize || iconSize}rpx`,
  48. color: elIconColor(index)
  49. }"
  50. ></view>
  51. <!-- 角标-->
  52. <tn-badge
  53. v-if="!item.out && (item.count || item.dot)"
  54. :dot="item.dot || false"
  55. backgroundColor="tn-bg-red"
  56. fontColor="#FFFFFF"
  57. :radius="item.dot ? 14 : 0"
  58. :fontSize="14"
  59. padding="2rpx 4rpx"
  60. :absolute="true"
  61. :top="2"
  62. >
  63. {{ $t.number.formatNumberString(item.count) }}
  64. </tn-badge>
  65. </view>
  66. <!-- tabbar item的文字-->
  67. <view
  68. class="tn-tabbar__content__item__text"
  69. :class="[elColor(index, false)]"
  70. :style="{
  71. color: elColor(index),
  72. fontSize: `${fontSize}rpx`
  73. }"
  74. >
  75. <text class="tn-text-ellipsis">{{ item.title }}</text>
  76. </view>
  77. </view>
  78. <!-- item 突起部分 -->
  79. <view
  80. v-if="outItemIndex !== -1"
  81. class="tn-tabbar__content__out"
  82. :class="[{
  83. 'tn-tabbar__content__out--shadow': shadow
  84. }, animation && value === outItemIndex ? `tn-tabbar__content__out--animation--${animationMode}` : '']"
  85. :style="{
  86. backgroundColor: bgColor,
  87. left: outItemLeft,
  88. width: `${outHeight}rpx`,
  89. height: `${outHeight}rpx`,
  90. top: `-${outHeight * 0.3}rpx`
  91. }"
  92. @tap.stop="clickItemHandler(outItemIndex)"
  93. ></view>
  94. </view>
  95. <!-- 防止tabbar塌陷 -->
  96. <view class="tn-tabbar__placeholder" :class="{'tn-safe-area-inset-bottom': safeAreaInsetBottom}" :style="{
  97. height: `calc(${height}rpx)`
  98. }"></view>
  99. </view>
  100. </template>
  101. <script>
  102. export default {
  103. name: 'tn-tabbar',
  104. props: {
  105. // 绑定当前被选中的current值
  106. value: {
  107. type: [String, Number],
  108. default: 0
  109. },
  110. // 是否显示
  111. show: {
  112. type: Boolean,
  113. default: true
  114. },
  115. // 图标列表
  116. list: {
  117. type: Array,
  118. default() {
  119. return []
  120. }
  121. },
  122. // 高度,单位rpx
  123. height: {
  124. type: Number,
  125. default: 100
  126. },
  127. // 突起的高度
  128. outHeight: {
  129. type: Number,
  130. default: 100
  131. },
  132. // 背景颜色
  133. bgColor: {
  134. type: String,
  135. default: '#FFFFFF'
  136. },
  137. // 图标大小
  138. iconSize: {
  139. type: Number,
  140. default: 40
  141. },
  142. // 字体大小
  143. fontSize: {
  144. type: Number,
  145. default: 24
  146. },
  147. // 激活时的颜色
  148. activeColor: {
  149. type: String,
  150. default: '#01BEFF'
  151. },
  152. // 非激活时的颜色
  153. inactiveColor: {
  154. type: String,
  155. default: '#AAAAAA'
  156. },
  157. // 激活时图标的颜色
  158. activeIconColor: {
  159. type: String,
  160. default: '#01BEFF'
  161. },
  162. // 非激活时图标的颜色
  163. inactiveIconColor: {
  164. type: String,
  165. default: '#AAAAAA'
  166. },
  167. // 激活时的自定义样式
  168. activeStyle: {
  169. type: Object,
  170. default() {
  171. return {}
  172. }
  173. },
  174. // 是否显示阴影
  175. shadow: {
  176. type: Boolean,
  177. default: true
  178. },
  179. // 点击时是否有动画
  180. animation: {
  181. type: Boolean,
  182. default: false
  183. },
  184. // 点击时的动画模式
  185. animationMode: {
  186. type: String,
  187. default: 'scale'
  188. },
  189. // 是否固定在底部
  190. fixed: {
  191. type: Boolean,
  192. default: true
  193. },
  194. // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
  195. safeAreaInsetBottom: {
  196. type: Boolean,
  197. default: false
  198. },
  199. // 切换前回调
  200. beforeSwitch: {
  201. type: Function,
  202. default: null
  203. }
  204. },
  205. computed: {
  206. // 当前字体的颜色
  207. elColor() {
  208. return (index, style = true) => {
  209. let currentItem = this.list[index]
  210. let color = ''
  211. if (index === this.value) {
  212. color = currentItem['activeColor'] || this.activeColor
  213. } else {
  214. color = currentItem['inactiveColor'] || this.inactiveColor
  215. }
  216. // 判断是否获取内部样式
  217. if (style) {
  218. if (this.$t.color.getFontColorStyle(color) !== '') {
  219. return color
  220. } else {
  221. return ''
  222. }
  223. } else {
  224. if (this.$t.color.getFontColorStyle(color) === '') {
  225. return color
  226. } else {
  227. return ''
  228. }
  229. }
  230. }
  231. },
  232. // 当前图标的颜色
  233. elIconColor() {
  234. return (index, style = true) => {
  235. let currentItem = this.list[index]
  236. let color = ''
  237. if (index === this.value) {
  238. color = currentItem['activeIconColor'] || this.activeIconColor
  239. } else {
  240. color = currentItem['inactiveIconColor'] || this.inactiveIconColor
  241. }
  242. // 判断是否获取内部样式
  243. if (style) {
  244. if (this.$t.color.getFontColorStyle(color) !== '') {
  245. return color
  246. } else {
  247. return ''
  248. }
  249. } else {
  250. if (this.$t.color.getFontColorStyle(color) === '') {
  251. return color + ' tn-tabbar__content__item__icon--clip'
  252. } else {
  253. return ''
  254. }
  255. }
  256. }
  257. },
  258. // 当前的图标
  259. elIcon() {
  260. return (index) => {
  261. let currentItem = this.list[index]
  262. if (index === this.value) {
  263. return currentItem['activeIcon']
  264. } else {
  265. return currentItem['inactiveIcon']
  266. }
  267. }
  268. },
  269. // 突起部分item button对应的类
  270. itemButtonClass() {
  271. return (index) => {
  272. let clazz = ''
  273. if (this.list[index]['out']) {
  274. clazz += 'tn-tabbar__content__item__button--out'
  275. if (this.$t.color.getFontColorStyle(this.activeIconColor) === '') {
  276. clazz += ` ${this.activeIconColor}`
  277. }
  278. if (this.value === index) {
  279. clazz += ` tn-tabbar__content__item__button--out--animation--${this.animationMode}`
  280. }
  281. } else {
  282. clazz += 'tn-tabbar__content__item__button'
  283. if (this.value === index) {
  284. clazz += ` tn-tabbar__content__item__button--animation--${this.animationMode}`
  285. }
  286. }
  287. return clazz
  288. }
  289. },
  290. // 突起部分item button样式
  291. itemButtonStyle() {
  292. return (index) => {
  293. let style = {}
  294. if (this.list[index]['out']) {
  295. if (this.$t.color.getFontColorStyle(this.activeIconColor) !== '') {
  296. style.backgroundColor = this.activeIconColor
  297. }
  298. style.width = `${this.outHeight - 35}rpx`
  299. style.height = `${this.outHeight - 35}rpx`
  300. style.top = `-${this.outHeight * 0.15}rpx`
  301. return style
  302. }
  303. return style
  304. }
  305. },
  306. // 判断图标是否为图片
  307. isImage() {
  308. return (index) => {
  309. const icon = this.list[index]['activeIcon']
  310. // 只有包含了'/'就认为是图片
  311. return icon.indexOf('/') !== -1
  312. }
  313. }
  314. },
  315. data() {
  316. return {
  317. // 当前突起的位置
  318. outItemLeft: '50%',
  319. // 当前设置了突起按钮的index
  320. outItemIndex: -1,
  321. // 每一个item的信息
  322. tabbatItemInfo: []
  323. }
  324. },
  325. watch: {
  326. },
  327. created() {
  328. this.getOutItemIndex()
  329. },
  330. mounted() {
  331. this.$nextTick(() => {
  332. this.getTabbarItem()
  333. })
  334. },
  335. methods: {
  336. // 获取每一个item的信息
  337. getTabbarItem() {
  338. let query = uni.createSelectorQuery().in(this)
  339. // 遍历获取信息
  340. for (let i = 0; i < this.list.length; i++) {
  341. query.select(`#tabbar_item_${i}`).fields({
  342. size: true,
  343. rect: true
  344. })
  345. }
  346. query.exec(res => {
  347. if (!res) {
  348. setTimeout(() => {
  349. this.getTabbarItem()
  350. }, 10)
  351. return
  352. }
  353. this.tabbatItemInfo = res.map((item) => {
  354. return {
  355. left: item.left,
  356. width: item.width
  357. }
  358. })
  359. this.updateOutItemLeft()
  360. })
  361. },
  362. // 获取突起Item所在的index(如果存在)
  363. getOutItemIndex() {
  364. this.outItemIndex = this.list.findIndex((item) => {
  365. return item.hasOwnProperty('out') && item.out
  366. })
  367. },
  368. // 点击底部菜单时触发
  369. async clickItemHandler(index) {
  370. if (this.beforeSwitch && typeof(this.beforeSwitch) === 'function') {
  371. // 执行回调,同时传入索引当作参数
  372. // 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
  373. // 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
  374. let beforeSwitch = this.beforeSwitch.bind(this.$t.$parent.call(this))(index)
  375. // 判断是否返回了Promise
  376. if (!!beforeSwitch && typeof beforeSwitch.then === 'function') {
  377. await beforeSwitch.then(res => {
  378. // Promise返回成功
  379. this.switchTab(index)
  380. }).catch(err => {
  381. })
  382. } else if (beforeSwitch === true) {
  383. this.switchTab(index)
  384. }
  385. } else {
  386. this.switchTab(index)
  387. }
  388. },
  389. // 切换tab
  390. switchTab(index) {
  391. // 发出事件和修改v-model绑定的值
  392. this.$emit('change', index)
  393. this.$emit('input', index)
  394. },
  395. // 设置突起的位置
  396. updateOutItemLeft() {
  397. // 查找出需要突起的元素
  398. const index = this.list.findIndex((item) => {
  399. return item.out
  400. })
  401. if (index !== -1) {
  402. this.outItemLeft = this.tabbatItemInfo[index].left + (this.tabbatItemInfo[index].width / 2) + 'px'
  403. }
  404. }
  405. }
  406. }
  407. </script>
  408. <style lang="scss" scoped>
  409. .tn-tabbar {
  410. &__content {
  411. box-sizing: content-box;
  412. display: flex;
  413. flex-direction: row;
  414. align-items: center;
  415. position: relative;
  416. width: 100%;
  417. z-index: 1024;
  418. &__out {
  419. position: absolute;
  420. z-index: 4;
  421. border-radius: 100%;
  422. left: 50%;
  423. transform: translateX(-50%);
  424. &--shadow {
  425. box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
  426. &::before {
  427. content: " ";
  428. position: absolute;
  429. width: 100%;
  430. height: 50rpx;
  431. bottom: 0;
  432. left: 0;
  433. right: 0;
  434. margin: auto;
  435. background-color: inherit;
  436. }
  437. }
  438. &--animation {
  439. &--scale {
  440. transform-origin: 50% 100%;
  441. animation:tabbar-content-out-click 0.2s forwards 1 ease-in-out;
  442. }
  443. }
  444. }
  445. &__item {
  446. flex: 1;
  447. display: flex;
  448. flex-direction: column;
  449. justify-content: flex-end;
  450. align-items: center;
  451. height: 100%;
  452. position: relative;
  453. &__button {
  454. margin-bottom: 10rpx;
  455. display: flex;
  456. align-items: center;
  457. justify-content: center;
  458. position: relative;
  459. &--out {
  460. margin-bottom: 10rpx;
  461. border-radius: 50%;
  462. position: absolute;
  463. display: flex;
  464. justify-content: center;
  465. align-items: center;
  466. z-index: 6;
  467. &--animation {
  468. &--scale {
  469. transform-origin: 50% 100%;
  470. animation:tabbar-item-button-out-click 0.2s forwards 1;
  471. }
  472. }
  473. }
  474. &--animation {
  475. &--scale {
  476. .tn-tabbar__content__item__icon, .tn-tabbar__content__item__image {
  477. transform-origin: 50% 100%;
  478. animation:tabbar-item-button-click 0.2s forwards 1;
  479. }
  480. }
  481. }
  482. }
  483. &__icon {
  484. &--clip {
  485. -webkit-background-clip: text;
  486. color: transparent !important;
  487. }
  488. }
  489. &__text {
  490. width: 100%;
  491. font-size: 26rpx;
  492. line-height: 28rpx;
  493. text-align: center;
  494. margin-bottom: 10rpx;
  495. z-index: 10;
  496. transition: all 0.2s ease-in-out;
  497. }
  498. &--out {
  499. height: calc(100% - 1px);
  500. }
  501. }
  502. }
  503. &--fixed {
  504. position: fixed;
  505. bottom: 0;
  506. left: 0;
  507. right: 0;
  508. }
  509. &--shadow {
  510. box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07);
  511. }
  512. }
  513. /* 点击动画 start */
  514. @keyframes tabbar-item-button-click{
  515. from{
  516. transform: scale(0.8);
  517. }
  518. to{
  519. transform: scale(1);
  520. }
  521. }
  522. @keyframes tabbar-item-button-out-click {
  523. 0%{
  524. transform: translateY(0) scale(1);
  525. }
  526. 50%{
  527. transform: translateY(-10rpx) scale(1.2);
  528. }
  529. 100%{
  530. transform: translateY(0) scale(1);
  531. }
  532. }
  533. @keyframes tabbar-content-out-click {
  534. 0%{
  535. transform: translateX(-50%) translateY(0) scale(1);
  536. }
  537. 50% {
  538. transform: translateX(-50%) translateY(-10rpx) scale(1.1);
  539. }
  540. 100% {
  541. transform: translateX(-50%) translateY(0) scale(1);
  542. }
  543. }
  544. /* 点击动画 end */
  545. </style>