349 lines
9.5 KiB
Vue
349 lines
9.5 KiB
Vue
|
<template>
|
|||
|
<view class="tn-tabs-class tn-tabs" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $tn.string.getLengthUnitValue(top, 'px')}">
|
|||
|
|
|||
|
<!-- _tgetRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 -->
|
|||
|
<view :id="id">
|
|||
|
<scroll-view scroll-x class="tn-tabs__scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
|
|||
|
<view class="tn-tabs__scroll-view__box" :class="{'tn-tabs__scroll-view--flex': !isScroll}">
|
|||
|
<!-- item -->
|
|||
|
<view
|
|||
|
v-for="(item, index) in list"
|
|||
|
:key="index"
|
|||
|
:id="'tn-tabs__scroll-view__item-' + index"
|
|||
|
class="tn-tabs__scroll-view__item tn-text-ellipsis"
|
|||
|
:style="[tabItemStyle(index)]"
|
|||
|
@tap="clickTab(index)"
|
|||
|
>
|
|||
|
<tn-badge v-if="item[count] || item['count']" backgroundColor="tn-bg-red" fontColor="#FFFFFF" :absolute="true" :top="badgeOffset[0] || 0" :right="badgeOffset[1] || 0">{{ item[count] || item['count']}}</tn-badge>
|
|||
|
{{ item[name] || item['name'] }}
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 底部滑块 -->
|
|||
|
<view v-if="showBar" class="tn-tabs__bar" :style="[tabBarStyle]"></view>
|
|||
|
</view>
|
|||
|
</scroll-view>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
import componentsColor from '../../libs/mixin/components_color.js'
|
|||
|
export default {
|
|||
|
mixins: [componentsColor],
|
|||
|
name: 'tn-tabs',
|
|||
|
props: {
|
|||
|
// 标签列表
|
|||
|
list: {
|
|||
|
type: Array,
|
|||
|
default() {
|
|||
|
return []
|
|||
|
}
|
|||
|
},
|
|||
|
// 列表数据tab名称的属性
|
|||
|
name: {
|
|||
|
type: String,
|
|||
|
default: 'name'
|
|||
|
},
|
|||
|
// 列表数据微标数量的属性
|
|||
|
count: {
|
|||
|
type: String,
|
|||
|
default: 'count'
|
|||
|
},
|
|||
|
// 当前活动的tab索引
|
|||
|
current: {
|
|||
|
type: Number,
|
|||
|
default: 0
|
|||
|
},
|
|||
|
// 菜单是否可以滑动
|
|||
|
isScroll: {
|
|||
|
type: Boolean,
|
|||
|
default: true
|
|||
|
},
|
|||
|
// 高度
|
|||
|
height: {
|
|||
|
type: Number,
|
|||
|
default: 80
|
|||
|
},
|
|||
|
// 距离顶部的距离(px)
|
|||
|
top: {
|
|||
|
type: Number,
|
|||
|
default: 0
|
|||
|
},
|
|||
|
// item的宽度
|
|||
|
itemWidth: {
|
|||
|
type: [String, Number],
|
|||
|
default: 'auto'
|
|||
|
},
|
|||
|
// 过渡动画时长
|
|||
|
duration: {
|
|||
|
type: Number,
|
|||
|
default: 0.3
|
|||
|
},
|
|||
|
// 选中时的颜色
|
|||
|
activeColor: {
|
|||
|
type: String,
|
|||
|
default: '#01BEFF'
|
|||
|
},
|
|||
|
// 未被选中时的颜色
|
|||
|
inactiveColor: {
|
|||
|
type: String,
|
|||
|
default: '#080808'
|
|||
|
},
|
|||
|
// 选中的item样式
|
|||
|
activeItemStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 未选中的item样式
|
|||
|
inactiveItemStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 是否显示底部滑块
|
|||
|
showBar: {
|
|||
|
type: Boolean,
|
|||
|
default: true
|
|||
|
},
|
|||
|
// 底部滑块的宽度
|
|||
|
barWidth: {
|
|||
|
type: Number,
|
|||
|
default: 40
|
|||
|
},
|
|||
|
// 底部滑块的高度
|
|||
|
barHeight: {
|
|||
|
type: Number,
|
|||
|
default: 6
|
|||
|
},
|
|||
|
// 自定义底部滑块的样式
|
|||
|
barStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 单个tab的左右内边距
|
|||
|
gutter: {
|
|||
|
type: Number,
|
|||
|
default: 30
|
|||
|
},
|
|||
|
// 微标的偏移数[top, right]
|
|||
|
badgeOffset: {
|
|||
|
type: Array,
|
|||
|
default() {
|
|||
|
return [20, 22]
|
|||
|
}
|
|||
|
},
|
|||
|
// 是否加粗字体
|
|||
|
bold: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
}
|
|||
|
},
|
|||
|
computed: {
|
|||
|
// 底部滑块样式
|
|||
|
tabBarStyle() {
|
|||
|
let style = {
|
|||
|
width: this.$tn.string.getLengthUnitValue(this.barWidth),
|
|||
|
height: this.$tn.string.getLengthUnitValue(this.barHeight),
|
|||
|
borderRadius: `${this.barHeight / 2}rpx`,
|
|||
|
backgroundColor: this.activeColor,
|
|||
|
opacity: this.barMoveFirst ? 0 : 1,
|
|||
|
transform: `translate(${this.scrollBarLeft}px, -100%)`,
|
|||
|
transitionDuration: this.barMoveFirst ? '0s' : `${this.duration}s`
|
|||
|
}
|
|||
|
Object.assign(style, this.barStyle)
|
|||
|
return style
|
|||
|
},
|
|||
|
// tabItem样式
|
|||
|
tabItemStyle() {
|
|||
|
return index => {
|
|||
|
let style = {
|
|||
|
width: this.$tn.string.getLengthUnitValue(this.itemWidth),
|
|||
|
height: this.$tn.string.getLengthUnitValue(this.height),
|
|||
|
lineHeight: this.$tn.string.getLengthUnitValue(this.height),
|
|||
|
fontSize: this.fontSizeStyle || '28rpx',
|
|||
|
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
|
|||
|
flex: this.isScroll ? 'auto' : '1',
|
|||
|
transitionDuration: `${this.duration}s`
|
|||
|
}
|
|||
|
if (index === this.currentIndex) {
|
|||
|
if (this.bold) {
|
|||
|
style.fontWeight = 'bold'
|
|||
|
}
|
|||
|
style.color = this.activeColor
|
|||
|
Object.assign(style, this.activeItemStyle)
|
|||
|
} else {
|
|||
|
style.color = this.inactiveColor
|
|||
|
Object.assign(style, this.inactiveItemStyle)
|
|||
|
}
|
|||
|
return style
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
data() {
|
|||
|
return {
|
|||
|
// id值
|
|||
|
id: this.$tn.uuid(),
|
|||
|
// 滚动scroll-view的左边距离
|
|||
|
scrollLeft: 0,
|
|||
|
// 存放查询后tab菜单的节点信息
|
|||
|
tabQueryInfo: [],
|
|||
|
// 组件宽度
|
|||
|
componentWidth: 0,
|
|||
|
// 底部滑块的移动距离
|
|||
|
scrollBarLeft: 0,
|
|||
|
// 组件到屏幕左边的巨鹿
|
|||
|
componentLeft: 0,
|
|||
|
// 当前选中的itemIndex
|
|||
|
currentIndex: this.current,
|
|||
|
// 标记底部滑块是否第一次移动,第一次移动的时候不触发动画
|
|||
|
barMoveFirst: true
|
|||
|
}
|
|||
|
},
|
|||
|
watch: {
|
|||
|
// 监听tab的变化,重新计算tab菜单信息
|
|||
|
list(newValue, oldValue) {
|
|||
|
// list变化时,重置内部索引,防止出现超过数据边界的问题
|
|||
|
if (newValue.length !== oldValue.length) this.currentIndex = 0
|
|||
|
this.$nextTick(() => {
|
|||
|
this.init()
|
|||
|
})
|
|||
|
},
|
|||
|
current: {
|
|||
|
handler(val) {
|
|||
|
this.$nextTick(() => {
|
|||
|
this.currentIndex = val
|
|||
|
this.scrollByIndex()
|
|||
|
})
|
|||
|
},
|
|||
|
immediate: true
|
|||
|
}
|
|||
|
},
|
|||
|
mounted() {
|
|||
|
this.init()
|
|||
|
},
|
|||
|
methods: {
|
|||
|
// 初始化变量
|
|||
|
async init() {
|
|||
|
// 获取tabs组件的信息
|
|||
|
let tabRect = await this._tGetRect('#' + this.id)
|
|||
|
// 计算组件的宽度
|
|||
|
this.componentLeft = tabRect.left
|
|||
|
this.componentWidth = tabRect.width
|
|||
|
this.getTabRect()
|
|||
|
},
|
|||
|
// 点击tab菜单
|
|||
|
clickTab(index) {
|
|||
|
if (index === this.currentIndex) return
|
|||
|
this.$emit('change', index)
|
|||
|
},
|
|||
|
// 查询tab的布局信息
|
|||
|
getTabRect() {
|
|||
|
let query = uni.createSelectorQuery().in(this)
|
|||
|
// 遍历所有的tab
|
|||
|
for (let i = 0; i < this.list.length; i++) {
|
|||
|
query.select(`#tn-tabs__scroll-view__item-${i}`).fields({
|
|||
|
size: true,
|
|||
|
rect: true
|
|||
|
})
|
|||
|
}
|
|||
|
query.exec((res) => {
|
|||
|
this.tabQueryInfo = res
|
|||
|
// 初始滚动条和底部滑块的位置
|
|||
|
this.scrollByIndex()
|
|||
|
})
|
|||
|
},
|
|||
|
// 滚动scrollView,让活动的tab处于屏幕中间
|
|||
|
scrollByIndex() {
|
|||
|
// 当前获取tab的布局信息
|
|||
|
let tabInfo = this.tabQueryInfo[this.currentIndex]
|
|||
|
if (!tabInfo) return
|
|||
|
|
|||
|
// 活动tab的宽度
|
|||
|
let tabWidth = tabInfo.width
|
|||
|
// 活动item的左边到组件左边的距离
|
|||
|
let offsetLeft = tabInfo.left - this.componentLeft
|
|||
|
// 计算scroll-view移动的距离
|
|||
|
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2
|
|||
|
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft
|
|||
|
|
|||
|
// 计算当前滑块需要移动的距离,当前活动item的中点到左边的距离减去滑块宽度的一半
|
|||
|
let left = tabInfo.left + tabInfo.width / 2 - this.componentLeft
|
|||
|
|
|||
|
// 计算当前活跃item到组件左边的距离
|
|||
|
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2
|
|||
|
|
|||
|
// 防止在计算时出错,所以延迟执行标记不是第一次移动
|
|||
|
if (this.barMoveFirst) {
|
|||
|
setTimeout(() => {
|
|||
|
this.barMoveFirst = false
|
|||
|
}, 100)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="scss" scoped>
|
|||
|
|
|||
|
/* #ifndef APP-NVUE */
|
|||
|
::-webkit-scrollbar {
|
|||
|
display: none;
|
|||
|
width: 0 !important;
|
|||
|
height: 0 !important;
|
|||
|
-webkit-appearance: none;
|
|||
|
background: transparent;
|
|||
|
}
|
|||
|
/* #endif */
|
|||
|
|
|||
|
/* #ifdef H5 */
|
|||
|
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
|
|||
|
scroll-view ::v-deep ::-webkit-scrollbar {
|
|||
|
display: none;
|
|||
|
width: 0 !important;
|
|||
|
height: 0 !important;
|
|||
|
-webkit-appearance: none;
|
|||
|
background: transparent;
|
|||
|
}
|
|||
|
/* #endif */
|
|||
|
|
|||
|
.tn-tabs {
|
|||
|
&__scroll-view {
|
|||
|
position: relative;
|
|||
|
width: 100%;
|
|||
|
white-space: nowrap;
|
|||
|
|
|||
|
&__box {
|
|||
|
position: relative;
|
|||
|
/* #ifdef MP-TOUTIAO */
|
|||
|
white-space: nowrap;
|
|||
|
/* #endif */
|
|||
|
}
|
|||
|
|
|||
|
&__item {
|
|||
|
position: relative;
|
|||
|
/* #ifndef APP-NVUE */
|
|||
|
display: inline-block;
|
|||
|
/* #endif */
|
|||
|
text-align: center;
|
|||
|
transition-property: background-color, color;
|
|||
|
}
|
|||
|
|
|||
|
&--flex {
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
justify-content: space-between;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&__bar {
|
|||
|
position: absolute;
|
|||
|
bottom: 0;
|
|||
|
}
|
|||
|
}
|
|||
|
</style>
|