InspectionCleaning/uni_modules/z-tabs/components/z-tabs/z-tabs.vue

737 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- z-tabs v0.2.5 by-ZXLee -->
<!-- github地址:https://github.com/SmileZXLee/uni-z-tabs -->
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?name=z-tabs -->
<!-- 反馈QQ群790460711 -->
<template name="z-tabs">
<view class="z-tabs-conatiner" :style="[{background:bgColor}, tabsStyle]">
<view class="z-tabs-left">
<slot name="left" />
</view>
<view ref="z-tabs-scroll-view-conatiner" class="z-tabs-scroll-view-conatiner">
<scroll-view ref="z-tabs-scroll-view" class="z-tabs-scroll-view" :scroll-x="true" :scroll-left="scrollLeft" :show-scrollbar="false" :scroll-with-animation="isFirstLoaded" @scroll="scroll">
<view class="z-tabs-list-container" :style="[tabsListStyle]">
<view class="z-tabs-list" :style="[tabsListStyle, {marginTop: -finalBottomSpace+'px'}]">
<view :ref="`z-tabs-item-${index}`" :id="`z-tabs-item-${index}`" class="z-tabs-item" :style="[tabStyle]" v-for="(item,index) in list" :key="index" @click="tabsClick(index,item)">
<view class="z-tabs-item-title-container">
<text :class="{'z-tabs-item-title':true,'z-tabs-item-title-disabled':item.disabled}"
:style="[{color:item.disabled?disabledColor:(currentIndex===index?activeColor:inactiveColor)},item.disabled?disabledStyle:(currentIndex===index?activeStyle:inactiveStyle)]">
{{item[nameKey]||item}}
</text>
<text v-if="item.badge&&_formatCount(item.badge.count).length" class="z-tabs-item-badge" :style="[badgeStyle]">{{_formatCount(item.badge.count)}}</text>
</view>
</view>
</view>
<view class="z-tabs-bottom" :style="[{width: tabsContainerWidth+'px', bottom: finalBottomSpace+'px'}]">
<view ref="z-tabs-bottom-dot" class="z-tabs-bottom-dot"
<!-- #ifndef APP-NVUE -->
:style="[{transform:`translateX(${bottomDotX}px)`,transition:dotTransition,background:activeColor},finalDotStyle]"
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
:style="[{background:activeColor},finalDotStyle]"
<!-- #endif -->
/>
</view>
</view>
</scroll-view>
</view>
<view class="z-tabs-right">
<slot name="right" />
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const weexDom = weex.requireModule('dom');
const weexAnimation = weex.requireModule('animation');
// #endif
import zTabsConfig from './config/index'
//获取默认配置信息
function _gc(key, defaultValue) {
let config = null;
if (zTabsConfig && Object.keys(zTabsConfig).length) {
config = zTabsConfig;
} else {
return defaultValue;
}
const value = config[_toKebab(key)];
return value === undefined ? defaultValue : value;
}
//驼峰转短横线
function _toKebab(value) {
return value.replace(/([A-Z])/g, "-$1").toLowerCase();
}
/**
* z-tabs 标签
* @description 一个简单轻量的tabs标签全平台兼容支持nvue、vue3
* @tutorial https://ext.dcloud.net.cn/plugin?name=z-tabs
* @property {Array} list 数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
* @property {Number|String} current 当前选中的index默认为0
* @property {Number|String} scroll-count list数组长度超过scrollCount时滚动显示(不自动铺满全屏)默认为5
* @property {Number|String} tab-width 自定义每个tab的宽度默认为0即代表根据内容自动撑开单位rpx支持传100、"100px"或"100rpx"
* @property {Number|String} bar-width 滑块宽度单位rpx支持传100、"100px"或"100rpx"
* @property {Number|String} bar-height 滑块高度单位rpx支持传100、"100px"或"100rpx"
* @property {Object} bar-style 滑块样式其中的width和height将被bar-width和bar-height覆盖
* @property {Number|String} bottom-space tabs与底部的间距单位rpx支持传100、"100px"或"100rpx"
* @property {String} bar-animate-mode 切换tab时滑块动画模式与swiper联动时有效点击切换tab时无效必须调用setDx。默认为line即切换tab时滑块宽度保持不变线性运动。可选值为worm即为类似毛毛虫蠕动效果
* @property {String} name-key list中item的name(标题)的key默认为name
* @property {String} value-key list中item的value的key默认为value
* @property {String} active-color 激活状态tab的颜色
* @property {String} inactive-color 未激活状态tab的颜色
* @property {String} disabled-color 禁用状态tab的颜色
* @property {Object} active-style 激活状态tab的样式
* @property {Object} inactive-style 未激活状态tab的样式
* @property {Object} disabled-style 禁用状态tab的样式
* @property {Number|String} badge-max-count 徽标数最大数字限制超过这个数字将变成badge-max-count+默认为99
* @property {Object} badge-style 徽标样式,例如可自定义背景色,字体等等
* @property {String} bg-color z-tabs背景色
* @property {Object} tabs-style z-tabs样式
* @property {Boolean} init-trigger-change 初始化时是否自动触发change事件
* @event {Function(index,value)} change tabs改变时触发index:当前切换到的indexvalue:当前切换到的value
* @example <z-tabs :list="list"></z-tabs>
*/
export default {
name: 'z-tabs',
data() {
return {
currentIndex: 0,
currentSwiperIndex: 0,
bottomDotX: -1,
bottomDotXForIndex: 0,
showBottomDot: false,
shouldSetDx: true,
barCalcedWidth: 0,
pxBarWidth: 0,
scrollLeft: 0,
tabsSuperWidth: uni.upx2px(750),
tabsWidth: uni.upx2px(750),
tabsHeight: uni.upx2px(80),
tabsLeft: 0,
tabsContainerWidth: 0,
itemNodeInfos: [],
isFirstLoaded: false,
currentScrollLeft: 0,
changeTriggerFailed: false,
currentChanged: false
};
},
props: {
//数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
list: {
type: Array,
default: function() {
return [];
}
},
//当前选中的index
current: {
type: [Number, String],
default: _gc('current',0)
},
//list数组长度超过scrollCount时滚动显示(不自动铺满全屏)
scrollCount: {
type: [Number, String],
default: _gc('scrollCount',5)
},
//z-tabs样式
tabsStyle: {
type: Object,
default: function() {
return _gc('tabsStyle',{})
}
},
//自定义每个tab的宽度默认为0即代表根据内容自动撑开单位rpx支持传100、"100px"或"100rpx"
tabWidth: {
type: [Number, String],
default: _gc('tabWidth',0)
},
//滑块宽度单位rpx支持传100、"100px"或"100rpx"
barWidth: {
type: [Number, String],
default: _gc('barWidth',45)
},
//滑块高度单位rpx支持传100、"100px"或"100rpx"
barHeight: {
type: [Number, String],
default: _gc('barHeight',8)
},
//滑块样式其中的width和height将被barWidth和barHeight覆盖
barStyle: {
type: Object,
default: function() {
return _gc('barStyle',{});
}
},
//tabs与底部的间距单位rpx支持传100、"100px"或"100rpx"
bottomSpace: {
type: [Number, String],
default: _gc('bottomSpace',8)
},
//切换tab时滑块动画模式与swiper联动时有效点击切换tab时无效必须调用setDx。默认为line即切换tab时滑块宽度保持不变线性运动。可选值为worm即为类似毛毛虫蠕动效果
barAnimateMode: {
type: String,
default: _gc('barAnimateMode','line')
},
//list中item的name(标题)的key
nameKey: {
type: String,
default: _gc('nameKey','name')
},
//list中item的value的key
valueKey: {
type: String,
default: _gc('valueKey','value')
},
//激活状态tab的颜色
activeColor: {
type: String,
default: _gc('activeColor','#007AFF')
},
//未激活状态tab的颜色
inactiveColor: {
type: String,
default: _gc('inactiveColor','#666666')
},
//禁用状态tab的颜色
disabledColor: {
type: String,
default: _gc('disabledColor','#bbbbbb')
},
//激活状态tab的样式
activeStyle: {
type: Object,
default: function() {
return _gc('activeStyle',{});
}
},
//未激活状态tab的样式
inactiveStyle: {
type: Object,
default: function() {
return _gc('inactiveStyle',{});
}
},
//禁用状态tab的样式
disabledStyle: {
type: Object,
default: function() {
return _gc('disabledStyle',{});
}
},
//z-tabs背景色
bgColor: {
type: String,
default: _gc('bgColor','white')
},
//徽标数最大数字限制超过这个数字将变成badgeMaxCount+
badgeMaxCount: {
type: [Number, String],
default: _gc('badgeMaxCount',99)
},
//徽标样式,例如可自定义背景色,字体等等
badgeStyle: {
type: Object,
default: function() {
return _gc('badgeStyle',{})
}
},
//初始化时是否自动触发change事件
initTriggerChange: {
type: Boolean,
default: _gc('initTriggerChange',false)
}
},
mounted() {
this.updateSubviewLayout();
},
watch: {
current: {
handler(newVal) {
this.currentChanged && this._lockDx();
this.currentIndex = newVal;
this._preUpdateDotPosition(this.currentIndex);
if (this.initTriggerChange) {
if (newVal < this.list.length) {
this.$emit('change', newVal, this.list[newVal][this.valueKey]);
}else {
this.changeTriggerFailed = true;
}
}
this.currentChanged = true;
},
immediate: true
},
list: {
handler(newVal) {
this._handleListChange(newVal);
},
immediate: false
},
bottomDotX(newVal) {
if(newVal >= 0){
// #ifndef APP-NVUE
this.showBottomDot = true;
// #endif
this.$nextTick(() => {
// #ifdef APP-NVUE
weexAnimation.transition(this.$refs['z-tabs-bottom-dot'], {
styles: {
transform: `translateX(${newVal}px)`
},
duration: this.showAnimate ? 200 : 0,
delay: 0
})
setTimeout(() => {
this.showBottomDot = true;
},10)
// #endif
})
}
},
finalBarWidth: {
handler(newVal) {
this.barCalcedWidth = newVal;
this.pxBarWidth = this.barCalcedWidth;
},
immediate: true
},
currentIndex: {
handler(newVal) {
this.currentSwiperIndex = newVal;
},
immediate: true
}
},
computed: {
shouldScroll(){
return this.list.length > this.scrollCount;
},
finalTabsHeight(){
return this.tabsHeight;
},
tabStyle(){
const stl = this.shouldScroll ? {'flex-shrink': 0} : {'flex': 1};
if(this.finalTabWidth > 0){
stl['width'] = this.finalTabWidth + 'px';
}else{
delete stl.width;
}
return stl;
},
tabsListStyle(){
return this.shouldScroll ? {} : {'flex':1};
},
showAnimate(){
return this.isFirstLoaded && !this.shouldSetDx;
},
dotTransition(){
return this.showAnimate ? 'transform .2s linear':'none';
},
finalDotStyle(){
return {...this.barStyle, width: this.barCalcedWidth + 'px', height: this.finalBarHeight + 'px', opacity: this.showBottomDot ? 1 : 0};
},
finalTabWidth(){
return this._convertTextToPx(this.tabWidth);
},
finalBarWidth(){
return this._convertTextToPx(this.barWidth);
},
finalBarHeight(){
return this._convertTextToPx(this.barHeight);
},
finalBottomSpace(){
return this._convertTextToPx(this.bottomSpace);
}
},
methods: {
//根据swiper的@transition实时更新底部dot位置
setDx(dx) {
if (!this.shouldSetDx) return;
const isLineMode = this.barAnimateMode === 'line';
const isWormMode = this.barAnimateMode === 'worm';
let dxRate = dx / this.tabsSuperWidth;
this.currentSwiperIndex = this.currentIndex + parseInt(dxRate);
const isRight = dxRate > 0;
const barWidth = this.pxBarWidth;
if(this.currentSwiperIndex !== this.currentIndex){
dxRate = dxRate - (this.currentSwiperIndex - this.currentIndex);
const currentNode = this.itemNodeInfos[this.currentSwiperIndex];
if (!!currentNode){
this.bottomDotXForIndex = this._getBottomDotX(currentNode, barWidth);
}
}
const currentIndex = this.currentSwiperIndex;
let nextIndex = currentIndex + (isRight ? 1 : -1);
nextIndex = Math.max(0, nextIndex);
nextIndex = Math.min(nextIndex, this.itemNodeInfos.length - 1);
const currentNodeInfo = this.itemNodeInfos[currentIndex];
const nextNodeInfo = this.itemNodeInfos[nextIndex];
const nextBottomX = nextNodeInfo ? this._getBottomDotX(nextNodeInfo, barWidth) : 0;
if (isLineMode){
this.bottomDotX = this.bottomDotXForIndex + (nextBottomX - this.bottomDotXForIndex) * Math.abs(dxRate);
} else if (isWormMode) {
if ((isRight && currentIndex >= this.itemNodeInfos.length - 1) || (!isRight && currentIndex <= 0)) return;
const spaceOffset = isRight ? nextNodeInfo.right - currentNodeInfo.left : currentNodeInfo.right - nextNodeInfo.left;
let barCalcedWidth = barWidth + spaceOffset * Math.abs(dxRate);
if (isRight) {
if (barCalcedWidth > nextBottomX - this.bottomDotX + barWidth) {
const barMinusWidth = barWidth + spaceOffset * (1 - dxRate);
this.bottomDotX = this.bottomDotXForIndex + (barCalcedWidth - barMinusWidth) / 2;
barCalcedWidth = barMinusWidth;
}
}else if (!isRight) {
if (barCalcedWidth > this.bottomDotXForIndex + barWidth - nextBottomX){
const barMinusWidth = barWidth + spaceOffset * (1 + dxRate);
barCalcedWidth = barMinusWidth;
this.bottomDotX = nextBottomX;
} else{
this.bottomDotX = this.bottomDotXForIndex - (barCalcedWidth - barWidth);
}
}
barCalcedWidth = Math.max(barCalcedWidth, barWidth);
this.barCalcedWidth = barCalcedWidth;
}
},
//在swiper的@animationfinish中通知z-tabs结束多setDx的锁定若在父组件中调用了setDx则必须调用unlockDx
unlockDx() {
this.$nextTick(() => {
this.shouldSetDx = true;
})
},
//更新z-tabs内部布局
updateSubviewLayout(tryCount = 0) {
this.$nextTick(() => {
let delayTime = 10;
// #ifdef APP-NVUE || MP-BAIDU
delayTime = 50;
// #endif
setTimeout(() => {
this._getNodeClientRect('.z-tabs-scroll-view-conatiner').then(res=>{
if (res){
if (!res[0].width && tryCount < 10) {
setTimeout(() => {
tryCount ++;
this.updateSubviewLayout(tryCount);
}, 50);
return;
}
this.tabsWidth = res[0].width;
this.tabsHeight = res[0].height;
this.tabsLeft = res[0].left;
this._handleListChange(this.list);
}
})
this._getNodeClientRect('.z-tabs-conatiner').then(res=>{
if(res && res[0].width){
this.tabsSuperWidth = res[0].width;
}
})
},delayTime)
})
},
//点击了tabs
tabsClick(index,item) {
if (item.disabled) return;
if (this.currentIndex != index) {
this.shouldSetDx = false;
this.$emit('change', index, item[this.valueKey]);
this.currentIndex = index;
this._preUpdateDotPosition(index);
} else {
this.$emit('secondClick',index, item[this.valueKey]);
}
},
//scroll-view滚动
scroll(e){
this.currentScrollLeft = e.detail.scrollLeft;
},
//锁定dx用于避免在swiper被动触发滚动时候执行setDx中的代码
_lockDx() {
this.shouldSetDx = false;
},
//更新底部dot位置之前的预处理
_preUpdateDotPosition(index) {
// #ifndef APP-NVUE
this.$nextTick(() => {
uni.createSelectorQuery().in(this).select(".z-tabs-scroll-view").fields({
scrollOffset: true
}, data => {
if (data) {
this.currentScrollLeft = data.scrollLeft;
this._updateDotPosition(index);
} else {
this._updateDotPosition(index);
}
}).exec();
})
// #endif
// #ifdef APP-NVUE
this._updateDotPosition(index);
// #endif
},
//更新底部dot位置
_updateDotPosition(index){
if(index >= this.itemNodeInfos.length) return;
this.$nextTick(async ()=>{
let node = this.itemNodeInfos[index];
let offset = 0;
let tabsContainerWidth = this.tabsContainerWidth;
if (JSON.stringify(this.activeStyle) !== '{}') {
const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${index}`,true);
if (nodeRes) {
node = nodeRes[0];
offset = this.currentScrollLeft;
this.tabsHeight = Math.max(node.height + uni.upx2px(28), this.tabsHeight);
tabsContainerWidth = 0;
for(let i = 0;i < this.itemNodeInfos.length;i++){
let oldNode = this.itemNodeInfos[i];
tabsContainerWidth += i === index ? node.width : oldNode.width;
}
}
}
this.bottomDotX = this._getBottomDotX(node, this.finalBarWidth, offset);
this.bottomDotXForIndex = this.bottomDotX;
if (this.tabsWidth) {
setTimeout(()=>{
let scrollLeft = this.bottomDotX - this.tabsWidth / 2 + this.finalBarWidth / 2;
scrollLeft = Math.max(0,scrollLeft);
if (tabsContainerWidth) {
scrollLeft = Math.min(scrollLeft,tabsContainerWidth - this.tabsWidth + 10);
}
if (this.shouldScroll && tabsContainerWidth > this.tabsWidth) {
this.scrollLeft = scrollLeft;
}
this.$nextTick(()=>{
this.isFirstLoaded = true;
})
},200)
}
})
},
// 处理list改变
_handleListChange(newVal) {
this.$nextTick(async ()=>{
if(newVal.length){
let itemNodeInfos = [];
let tabsContainerWidth = 0;
let delayTime = 0;
// #ifdef MP-BAIDU
delayTime = 100;
// #endif
setTimeout(async()=>{
for(let i = 0;i < newVal.length;i++){
const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${i}`,true);
if(nodeRes){
const node = nodeRes[0];
node.left += this.currentScrollLeft;
itemNodeInfos.push(node);
tabsContainerWidth += node.width;
}
if (i === this.currentIndex){
this.itemNodeInfos = itemNodeInfos;
this.tabsContainerWidth = tabsContainerWidth;
this._updateDotPosition(this.currentIndex);
}
}
this.itemNodeInfos = itemNodeInfos;
this.tabsContainerWidth = tabsContainerWidth;
this._updateDotPosition(this.currentIndex);
},delayTime)
}
})
if (this.initTriggerChange && this.changeTriggerFailed && newVal.length) {
if (this.current < newVal.length) {
this.$emit('change', this.current, newVal[this.current][this.valueKey]);
}
}
},
//根据node获取bottomX
_getBottomDotX(node, barWidth = this.finalBarWidth, offset = 0){
return node.left + node.width / 2 - barWidth / 2 + offset - this.tabsLeft;
},
//获取节点信息
_getNodeClientRect(select, withRefArr = false) {
// #ifdef APP-NVUE
select = select.replace('.', '').replace('#', '');
const ref = withRefArr ? this.$refs[select][0] : this.$refs[select];
return new Promise((resolve, reject) => {
if (ref) {
weexDom.getComponentRect(ref, option => {
if (option && option.result) {
resolve([option.size]);
} else resolve(false);
})
} else resolve(false);
});
return;
// #endif
const res = uni.createSelectorQuery().in(this);
res.select(select).boundingClientRect();
return new Promise((resolve, reject) => {
res.exec(data => {
resolve((data && data != '' && data != undefined && data.length) ? data : false);
});
});
},
//格式化badge中的count
_formatCount(count) {
if (!count) return '';
if (count > this.badgeMaxCount) {
return this.badgeMaxCount + '+';
}
return count.toString();
},
//将文本的px或者rpx转为px的值
_convertTextToPx(text) {
const dataType = Object.prototype.toString.call(text);
if (dataType === '[object Number]') {
return uni.upx2px(text);
}
let isRpx = false;
if (text.indexOf('rpx') !== -1 || text.indexOf('upx') !== -1) {
text = text.replace('rpx', '').replace('upx', '');
isRpx = true;
} else if (text.indexOf('px') !== -1) {
text = text.replace('px', '');
} else {
text = uni.upx2px(text);
}
if (!isNaN(text)) {
if (isRpx) return Number(uni.upx2px(text));
return Number(text);
}
return 0;
}
}
}
</script>
<style scoped>
.z-tabs-conatiner{
/* #ifndef APP-NVUE */
overflow: hidden;
display: flex;
width: 100%;
/* #endif */
/* #ifdef APP-NVUE */
width: 750rpx;
/* #endif */
flex-direction: row;
height: 80rpx;
}
.z-tabs-scroll-view-conatiner{
flex: 1;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
height: 100%;
width: 100%;
/* #endif */
flex-direction: row;
}
/* #ifndef APP-NVUE */
.z-tabs-scroll-view ::-webkit-scrollbar {
display: none;
-webkit-appearance: none;
width: 0 !important;
height: 0 !important;
background: transparent;
}
/* #endif */
.z-tabs-scroll-view{
flex-direction: row;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
/* #endif */
flex: 1;
}
.z-tabs-list-container{
position: relative;
/* #ifndef APP-NVUE */
height: 100%;
/* #endif */
}
.z-tabs-list,.z-tabs-list-container{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.z-tabs-item{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0px 20rpx;
}
.z-tabs-item-title-container{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
}
.z-tabs-item-title{
font-size: 30rpx;
}
.z-tabs-item-title-disabled{
/* #ifndef APP-NVUE */
cursor: not-allowed;
/* #endif */
}
.z-tabs-item-badge{
margin-left: 8rpx;
background-color: #ec5b56;
color: white;
font-size: 22rpx;
border-radius: 100px;
padding: 0rpx 10rpx;
}
.z-tabs-bottom{
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.z-tabs-bottom-dot{
border-radius: 100px;
}
.z-tabs-left,.z-tabs-right{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
}
</style>