549 lines
13 KiB
Vue
549 lines
13 KiB
Vue
<template>
|
||
<view v-if="show" class="tn-tabbar-class tn-tabbar" @touchmove.stop.prevent="() => {}">
|
||
<!-- tabbar 内容-->
|
||
<view class="tn-tabbar__content" :class="{
|
||
'tn-tabbar--fixed': fixed,
|
||
'tn-safe-area-inset-bottom': safeAreaInsetBottom,
|
||
'tn-tabbar--shadow': shadow
|
||
}" :style="{
|
||
height: height + 'rpx',
|
||
backgroundColor: bgColor
|
||
}">
|
||
<!-- tabbar item -->
|
||
<view v-for="(item, index) in list" :key="index" class="tn-tabbar__content__item"
|
||
:id="`tabbar_item_${index}`" :class="{'tn-tabbar__content__item--out': item.out}" :style="{
|
||
backgroundColor: bgColor
|
||
}" @tap.stop="clickItemHandler(index)">
|
||
<!-- tabbar item的图片或者icon-->
|
||
<view :class="[itemButtonClass(index)]" :style="[itemButtonStyle(index)]">
|
||
<image v-if="isImage(index)" :src="elIcon(index)" mode="scaleToFill"
|
||
class="tn-tabbar__content__item__image" :style="{
|
||
width: `${item.iconSize || iconSize}rpx`,
|
||
height: `${item.iconSize || iconSize}rpx`
|
||
}"></image>
|
||
<view v-else class="tn-tabbar__content__item__icon"
|
||
:class="[`tn-icon-${elIcon(index)}`,elIconColor(index, false)]" :style="{
|
||
fontSize: `${item.iconSize || iconSize}rpx`,
|
||
color: elIconColor(index)
|
||
}"></view>
|
||
|
||
<!-- 角标-->
|
||
<tn-badge v-if="!item.out && (item.count || item.dot)" :dot="item.dot || false"
|
||
backgroundColor="tn-bg-red" fontColor="#FFFFFF" :radius="item.dot ? 14 : 0" :fontSize="14"
|
||
padding="2rpx 4rpx" :absolute="true" :top="2">
|
||
{{ $tn.number.formatNumberString(item.count) }}
|
||
</tn-badge>
|
||
</view>
|
||
|
||
<!-- tabbar item的文字-->
|
||
<view class="tn-tabbar__content__item__text" :class="[elColor(index, false)]" :style="{
|
||
color: elColor(index),
|
||
fontSize: `${fontSize}rpx`
|
||
}">
|
||
<text class="tn-text-ellipsis">{{ item.title }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- item 突起部分 -->
|
||
<view v-if="outItemIndex !== -1" class="tn-tabbar__content__out" :class="[{
|
||
'tn-tabbar__content__out--shadow': shadow
|
||
}, animation && value === outItemIndex ? `tn-tabbar__content__out--animation--${animationMode}` : '']" :style="{
|
||
backgroundColor: `transparent`,
|
||
left: outItemLeft,
|
||
width: `${outHeight}rpx`,
|
||
height: `${outHeight}rpx`,
|
||
top: `-${outHeight * 0.3}rpx`
|
||
}" @tap.stop="clickItemHandler(outItemIndex)"></view>
|
||
</view>
|
||
|
||
<!-- 防止tabbar塌陷 -->
|
||
<view class="tn-tabbar__placeholder" :class="{'tn-safe-area-inset-bottom': safeAreaInsetBottom}" :style="{
|
||
height: `calc(${height}rpx)`
|
||
}"></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'tn-tabbar',
|
||
props: {
|
||
// 绑定当前被选中的current值
|
||
value: {
|
||
type: [String, Number],
|
||
default: 0
|
||
},
|
||
// 是否显示
|
||
show: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 图标列表
|
||
list: {
|
||
type: Array,
|
||
default () {
|
||
return []
|
||
}
|
||
},
|
||
// 高度,单位rpx
|
||
height: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
// 突起的高度
|
||
outHeight: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
// 背景颜色
|
||
bgColor: {
|
||
type: String,
|
||
default: '#FFFFFF'
|
||
},
|
||
// 图标大小
|
||
iconSize: {
|
||
type: Number,
|
||
default: 40
|
||
},
|
||
// 字体大小
|
||
fontSize: {
|
||
type: Number,
|
||
default: 24
|
||
},
|
||
// 激活时的颜色
|
||
activeColor: {
|
||
type: String,
|
||
default: '#01BEFF'
|
||
},
|
||
// 非激活时的颜色
|
||
inactiveColor: {
|
||
type: String,
|
||
default: '#AAAAAA'
|
||
},
|
||
// 激活时图标的颜色
|
||
activeIconColor: {
|
||
type: String,
|
||
default: '#01BEFF'
|
||
},
|
||
// 非激活时图标的颜色
|
||
inactiveIconColor: {
|
||
type: String,
|
||
default: '#AAAAAA'
|
||
},
|
||
// 激活时的自定义样式
|
||
activeStyle: {
|
||
type: Object,
|
||
default () {
|
||
return {}
|
||
}
|
||
},
|
||
// 是否显示阴影
|
||
shadow: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 点击时是否有动画
|
||
animation: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 点击时的动画模式
|
||
animationMode: {
|
||
type: String,
|
||
default: 'scale'
|
||
},
|
||
// 是否固定在底部
|
||
fixed: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
|
||
safeAreaInsetBottom: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 切换前回调
|
||
beforeSwitch: {
|
||
type: Function,
|
||
default: null
|
||
}
|
||
},
|
||
computed: {
|
||
// 当前字体的颜色
|
||
elColor() {
|
||
return (index, style = true) => {
|
||
let currentItem = this.list[index]
|
||
let color = ''
|
||
if (index === this.value) {
|
||
color = currentItem['activeColor'] || this.activeColor
|
||
} else {
|
||
color = currentItem['inactiveColor'] || this.inactiveColor
|
||
}
|
||
// 判断是否获取内部样式
|
||
if (style) {
|
||
if (this.$tn.color.getFontColorStyle(color) !== '') {
|
||
return color
|
||
} else {
|
||
return ''
|
||
}
|
||
} else {
|
||
if (this.$tn.color.getFontColorStyle(color) === '') {
|
||
return color
|
||
} else {
|
||
return ''
|
||
}
|
||
}
|
||
}
|
||
},
|
||
// 当前图标的颜色
|
||
elIconColor() {
|
||
return (index, style = true) => {
|
||
let currentItem = this.list[index]
|
||
let color = ''
|
||
if (index === this.value) {
|
||
color = currentItem['activeIconColor'] || this.activeIconColor
|
||
} else {
|
||
color = currentItem['inactiveIconColor'] || this.inactiveIconColor
|
||
}
|
||
// 判断是否获取内部样式
|
||
if (style) {
|
||
if (this.$tn.color.getFontColorStyle(color) !== '') {
|
||
return color
|
||
} else {
|
||
return ''
|
||
}
|
||
} else {
|
||
if (this.$tn.color.getFontColorStyle(color) === '') {
|
||
return color + ' tn-tabbar__content__item__icon--clip'
|
||
} else {
|
||
return ''
|
||
}
|
||
}
|
||
}
|
||
},
|
||
// 当前的图标
|
||
elIcon() {
|
||
return (index) => {
|
||
let currentItem = this.list[index]
|
||
if (index === this.value) {
|
||
return currentItem['activeIcon']
|
||
} else {
|
||
return currentItem['inactiveIcon']
|
||
}
|
||
}
|
||
},
|
||
// 突起部分item button对应的类
|
||
itemButtonClass() {
|
||
return (index) => {
|
||
let clazz = ''
|
||
if (this.list[index]['out']) {
|
||
clazz += 'tn-tabbar__content__item__button--out'
|
||
if (this.$tn.color.getFontColorStyle(this.activeIconColor) === '') {
|
||
clazz += ` ${this.activeIconColor}`
|
||
}
|
||
if (this.value === index) {
|
||
clazz += ` tn-tabbar__content__item__button--out--animation--${this.animationMode}`
|
||
}
|
||
} else {
|
||
clazz += 'tn-tabbar__content__item__button'
|
||
if (this.value === index) {
|
||
clazz += ` tn-tabbar__content__item__button--animation--${this.animationMode}`
|
||
}
|
||
}
|
||
return clazz
|
||
}
|
||
},
|
||
// 突起部分item button样式
|
||
itemButtonStyle() {
|
||
return (index) => {
|
||
let style = {}
|
||
console.log();
|
||
if (this.list[index]['out']) {
|
||
if (this.$tn.color.getFontColorStyle(this.activeIconColor) !== '') {
|
||
//style.backgroundColor = this.activeIconColor
|
||
if (this.value == 2) {
|
||
style.backgroundColor = '#ffffff'
|
||
} else {
|
||
style.backgroundColor = '#ffffff'
|
||
}
|
||
}
|
||
style.width = `${this.outHeight - 20}rpx`
|
||
style.height = `${this.outHeight - 20}rpx`
|
||
style.top = `-${this.outHeight * 0.3}rpx`
|
||
|
||
return style
|
||
}
|
||
return style
|
||
}
|
||
},
|
||
// 判断图标是否为图片
|
||
isImage() {
|
||
return (index) => {
|
||
const icon = this.list[index]['activeIcon']
|
||
// 只有包含了'/'就认为是图片
|
||
return icon.indexOf('/') !== -1
|
||
}
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
// 当前突起的位置
|
||
outItemLeft: '50%',
|
||
// 当前设置了突起按钮的index
|
||
outItemIndex: -1,
|
||
// 每一个item的信息
|
||
tabbatItemInfo: []
|
||
}
|
||
},
|
||
watch: {
|
||
|
||
},
|
||
created() {
|
||
this.getOutItemIndex()
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => {
|
||
this.getTabbarItem()
|
||
})
|
||
},
|
||
methods: {
|
||
// 获取每一个item的信息
|
||
getTabbarItem() {
|
||
let query = uni.createSelectorQuery().in(this)
|
||
// 遍历获取信息
|
||
for (let i = 0; i < this.list.length; i++) {
|
||
query.select(`#tabbar_item_${i}`).fields({
|
||
size: true,
|
||
rect: true
|
||
})
|
||
}
|
||
query.exec(res => {
|
||
if (!res) {
|
||
setTimeout(() => {
|
||
this.getTabbarItem()
|
||
}, 10)
|
||
return
|
||
}
|
||
this.tabbatItemInfo = res.map((item) => {
|
||
return {
|
||
left: item.left,
|
||
width: item.width
|
||
}
|
||
})
|
||
this.updateOutItemLeft()
|
||
})
|
||
},
|
||
// 获取突起Item所在的index(如果存在)
|
||
getOutItemIndex() {
|
||
this.outItemIndex = this.list.findIndex((item) => {
|
||
return item.hasOwnProperty('out') && item.out
|
||
})
|
||
},
|
||
// 点击底部菜单时触发
|
||
async clickItemHandler(index) {
|
||
if (this.beforeSwitch && typeof(this.beforeSwitch) === 'function') {
|
||
// 执行回调,同时传入索引当作参数
|
||
// 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
|
||
// 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
|
||
let beforeSwitch = this.beforeSwitch.bind(this.$tn.$parent.call(this))(index)
|
||
// 判断是否返回了Promise
|
||
if (!!beforeSwitch && typeof beforeSwitch.then === 'function') {
|
||
await beforeSwitch.then(res => {
|
||
// Promise返回成功
|
||
this.switchTab(index)
|
||
}).catch(err => {
|
||
|
||
})
|
||
} else if (beforeSwitch === true) {
|
||
this.switchTab(index)
|
||
}
|
||
} else {
|
||
this.switchTab(index)
|
||
}
|
||
},
|
||
// 切换tab
|
||
switchTab(index) {
|
||
// 发出事件和修改v-model绑定的值
|
||
this.$emit('change', index)
|
||
this.$emit('input', index)
|
||
},
|
||
// 设置突起的位置
|
||
updateOutItemLeft() {
|
||
// 查找出需要突起的元素
|
||
const index = this.list.findIndex((item) => {
|
||
return item.out
|
||
})
|
||
if (index !== -1) {
|
||
this.outItemLeft = this.tabbatItemInfo[index].left + (this.tabbatItemInfo[index].width / 2) + 'px'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.tn-tabbar {
|
||
|
||
&__content {
|
||
box-sizing: content-box;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
position: relative;
|
||
width: 100%;
|
||
z-index: 1024;
|
||
|
||
&__out {
|
||
position: absolute;
|
||
z-index: 4;
|
||
border-radius: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
|
||
&--shadow {
|
||
// box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
|
||
|
||
&::before {
|
||
content: " ";
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 50rpx;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
margin: auto;
|
||
background-color: inherit;
|
||
}
|
||
}
|
||
|
||
&--animation {
|
||
&--scale {
|
||
transform-origin: 50% 100%;
|
||
animation: tabbar-content-out-click 0.2s forwards 1 ease-in-out;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100%;
|
||
position: relative;
|
||
align-items: center;
|
||
|
||
&__button {
|
||
margin-bottom: 10rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
|
||
&--out {
|
||
margin-bottom: 10rpx;
|
||
border-radius: 50%;
|
||
position: absolute;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 6;
|
||
|
||
&--animation {
|
||
&--scale {
|
||
transform-origin: 50% 100%;
|
||
animation: tabbar-item-button-out-click 0.2s forwards 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
&--animation {
|
||
&--scale {
|
||
|
||
.tn-tabbar__content__item__icon,
|
||
.tn-tabbar__content__item__image {
|
||
transform-origin: 50% 100%;
|
||
animation: tabbar-item-button-click 0.2s forwards 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&__icon {
|
||
|
||
&--clip {
|
||
-webkit-background-clip: text;
|
||
color: transparent !important;
|
||
}
|
||
}
|
||
|
||
&__text {
|
||
width: 100%;
|
||
font-size: 26rpx;
|
||
line-height: 28rpx;
|
||
text-align: center;
|
||
margin-bottom: 10rpx;
|
||
z-index: 10;
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
&--out {
|
||
height: calc(100% - 1px);
|
||
}
|
||
}
|
||
}
|
||
|
||
&--fixed {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
}
|
||
|
||
&--shadow {
|
||
box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07);
|
||
}
|
||
}
|
||
|
||
/* 点击动画 start */
|
||
|
||
@keyframes tabbar-item-button-click {
|
||
from {
|
||
transform: scale(0.8);
|
||
}
|
||
|
||
to {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes tabbar-item-button-out-click {
|
||
0% {
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: translateY(-10rpx) scale(1.2);
|
||
}
|
||
|
||
100% {
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes tabbar-content-out-click {
|
||
0% {
|
||
transform: translateX(-50%) translateY(0) scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: translateX(-50%) translateY(-10rpx) scale(1.1);
|
||
}
|
||
|
||
100% {
|
||
transform: translateX(-50%) translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
/* 点击动画 end */
|
||
</style> |