封装缘由
项目要求让tabs的元素居中展示,但是tabs数组中的数据又比较少,而UI组件库(例如uview)的tabs组件是没有居中展示的功能的
效果
type为1时

type为2时

参数
| 组件属性 | 说明 | 示例 |
|---|---|---|
| type | tabs组件内容展示的对齐方式 | 字符串:1 tabs内容很多,超过屏幕宽度、2 tabs内容很少,不超过屏幕宽度,tabs内容居中显示 |
| tabList | tab数据列表 | 数组 |
| activeTab | 当前激活的tab的Id | Number数字 |
| isSticky | tabs组件是否粘性定位 | Boolean |
| tabsBoxStyle | 自定义tabs组件的样式 | Object |
| tabStyle | 自定义tab样式 | Object |
| tabActiveStyle | 自定义tab激活样式 | Object |
| tabsLineStyle | 自定义底部滑块样式 | Object |
| tabLineImg | 底部滑块图片地址(网络图片) | String |
| badgeShow | 徽标是否显示 | Boolean |
| badgeStyle | 自定义徽标样式 | Object |
| delayTime | 延迟时间,单位:ms | Number数字 |
| proxyField | 代理字段 | Object |
参数解释
proxyField:
用于指定组件中指定字段对应的是tabList中哪个字段
数据格式如下:
id用来区分选中的是哪个tab,name则是tab数据的展示字段,badge则是徽标的数据
注意事项
- 缺点:tabs组件初始化时会延迟一定时间出现,延迟时间:delayTime * tabList的数据量
- 如果遇到底部滑块定位不准确的问题,请增加delayTime的值
- 如果使用徽标:需要将徽标数据放在tabList中
tabs组件代码
<template>
<div
class="tabs-box"
:style="[tabsBoxStyle]"
>
<div
class="init-box"
v-if="!initChangeTab"
>
初始化中...
</div>
<scroll-view
:style="[scrollViewStyle, { width: '100%' }]"
scroll-x
scroll-with-animation
enable-flex
:scroll-left="scrollLeft"
>
<div
class="tabs"
:style="{
'justify-content': props.type == 2 ? 'center' : '',
opacity: initChangeTab ? 1 : 0,
}"
>
<!-- tab元素 开始 -->
<div
class="tab"
:style="[
tabStyle,
item[props.proxyField.id] === props.activeTab && initActiveTab === -1
? tabActiveStyle
: '',
item[props.proxyField.id] === initActiveTab ? tabActiveStyle : '',
{
transition: initChangeTab ? 'all 0.6s' : 'none',
},
]"
v-for="(item, index) in tabList"
:key="item[props.proxyField.id]"
@click="changeTab(index)"
>
{{ item[props.proxyField.name] }}
<!-- tab-line-test 用于查看底部滑块位置是否正确 -->
<!-- <div class="tab-line-test"></div> -->
<!-- 徽标 开始 -->
<div
class="badge-box"
:style="[badgeStyle]"
v-if="props.badgeShow && item[props.proxyField.badge]"
>
{{ item[props.proxyField.badge] }}
</div>
<!-- 徽标 结束 -->
</div>
<!-- tab元素 结束 -->
<!-- 底部滑块 开始 -->
<image
class="tabs-line"
:style="[tabsLineStyle]"
:src="props.tabLineImg"
mode=""
v-if="props.tabLineImg"
/>
<div
class="tabs-line"
:style="[tabsLineStyle]"
v-else
></div>
<!-- 底部滑块 结束 -->
</div>
</scroll-view>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
const props = defineProps({
/*
1 tabs内容很多,超过屏幕宽度
2 tabs内容很少,不超过屏幕宽度,tabs内容居中显示
*/
type: {
type: String,
default: '1',
},
// tab列表
tabList: {
type: Array,
required: true,
},
// 当前激活的tab
activeTab: {
type: [Number, String],
required: true,
},
// 是否粘性定位
isSticky: {
type: Boolean,
default: false,
},
// tabs-box样式
tabsBoxStyle: {
type: Object,
default: {},
},
// tab样式
tabStyle: {
type: Object,
default: {},
},
// tab激活样式
tabActiveStyle: {
type: Object,
default: {},
},
// 底部滑块样式
tabsLineStyle: {
type: Object,
default: {},
},
// 滑块图片地址(网络图片)
tabLineImg: {
type: String,
default: '',
},
// 徽标是否显示
badgeShow: {
type: Boolean,
default: false,
},
// 自定义徽标样式
badgeStyle: {
type: Object,
default: {},
},
// 延迟时间
delayTime: {
type: Number,
default: 150,
},
// 代理字段
proxyField: {
type: Object,
default: {
id: 'id',
name: 'name',
badge: 'badge',
},
},
});
const emit = defineEmits(['changeTab']);
const tabList = ref([]);
const initChangeTab = ref(false);
onMounted(async () => {
tabList.value = [...props.tabList];
await init();
await getAllLine();
initActiveTab.value = -1;
// 计算第一次的位置
let index = tabList.value.findIndex((ele) => ele[props.proxyField.id] === props.activeTab);
changeTab(index, 2);
initChangeTab.value = true;
});
watch(
() => props.tabList,
async () => {
initChangeTab.value = false;
scrollLeft.value = 0;
tabInfo = [];
tabsLineInfo = [];
initActiveTab.value = -1;
tabList.value = [...props.tabList];
await getAllLine();
initActiveTab.value = -1;
// 计算第一次的位置
let index = tabList.value.findIndex((ele) => ele[props.proxyField.id] === props.activeTab);
changeTab(index, 2);
initChangeTab.value = true;
},
{ deep: true }
);
// 初始化
async function init() {
tabsBoxStyle.value = {
...props.tabsBoxStyle,
position: props.isSticky ? 'sticky' : props.tabsBoxStyle.position || 'relative',
top: props.tabsBoxStyle.top || 0,
};
tabStyle.value = {
height: '80rpx',
padding: '0 20rpx',
display: 'flex',
'justify-content': 'center',
'align-items': 'center',
'font-weight': 500,
'font-size': '28rpx',
color: '#999',
...props.tabStyle,
};
tabActiveStyle.value = {
'font-weight': 800,
'font-size': '36rpx',
color: '#333',
...props.tabActiveStyle,
};
tabsLineStyle.value = {
height: '4rpx',
bottom: '-4rpx',
background: props.tabLineImg ? 'transparent' : '#fdd100',
...props.tabsLineStyle,
};
let scrollViewHeight = 0;
let tabHeight = 0;
let tabsLineHeight = 0;
let tabHeightRes = extractNumberAndUnit(tabStyle.value.height);
let tabsLineHeightRes = extractNumberAndUnit(tabsLineStyle.value.height);
switch (tabHeightRes.unit) {
case 'rpx':
tabHeight = tabHeightRes.number;
break;
case 'px':
tabHeight = tabHeightRes.number * 2;
break;
}
switch (tabsLineHeightRes.unit) {
case 'rpx':
tabsLineHeight = tabsLineHeightRes.number;
break;
case 'px':
tabsLineHeight = tabsLineHeightRes.number * 2;
break;
}
scrollViewHeight = tabHeight + tabsLineHeight;
scrollViewStyle.value = {
height: scrollViewHeight + 'rpx',
};
badgeStyle.value = {
...props.badgeStyle,
};
tabsBoxInfo = await getTabsBoxInfo();
}
function extractNumberAndUnit(inputString) {
const match = inputString.match(/(\d+)([a-zA-Z]+)/);
if (match) {
const number = match[1] * 1;
const unit = match[2];
return { number, unit };
} else {
return { number: null, unit: null };
}
}
// 获取tabs-box的宽度
function getTabsBoxInfo() {
let query = uni.createSelectorQuery().in(proxy);
return new Promise((resolve, reject) => {
query
.select('.tabs-box')
.boundingClientRect((data) => {
resolve({
left: data.left,
width: data.width,
});
})
.exec();
});
}
// 遍历所有选中状态下的底部滑块的位移距离
function getAllLine() {
return new Promise((resolve) => {
setTimeout(async () => {
for (let i = 0; i < tabList.value.length; i++) {
await changeTab(i, 1);
}
resolve();
}, props.delayTime);
});
}
let scrollViewStyle = ref({});
let scrollLeft = ref(0);
let tabsBoxStyle = ref({});
let tabStyle = ref({});
let tabActiveStyle = ref({});
let tabsLineStyle = ref({});
let badgeStyle = ref({});
let tabsBoxInfo = { left: '', width: '' };
let tabInfo = [];
let tabsLineInfo = [];
let initActiveTab = ref(-1);
// 切换tab
// mode : 0 用户操作 1 初始化 2 重置
async function changeTab(index, mode = 0) {
let selectTab = tabList.value[index];
if (selectTab[props.proxyField.id] != props.activeTab || mode) {
if (!mode) {
emit('changeTab', selectTab);
}
if (mode == 1) {
initActiveTab.value = selectTab[props.proxyField.id];
await getTabInfo();
if (props.type == 1) {
getLine1(index);
} else {
getLine2(index);
}
} else {
await new Promise((resolve) => setTimeout(resolve, props.delayTime));
await getTabInfo();
let data = tabsLineInfo[index];
tabsLineStyle.value.width = data.width;
// tabsLineStyle.value.transform = data.transform;
tabsLineStyle.value.left = data.moveX + 'px';
if (props.type == 1) {
if (mode == 1) {
return;
}
scrollLeft.value = index ? data.moveX - tabsBoxInfo.width / 2 : 0;
}
}
}
}
// 获取tab的信息
function getTabInfo() {
let query = uni.createSelectorQuery().in(proxy);
return new Promise((resolve, reject) => {
setTimeout(() => {
query
.selectAll('.tab')
.boundingClientRect((rect) => {
tabInfo = [];
tabInfo = rect.map((item) => {
return { width: item.width, left: item.left };
});
resolve();
})
.exec();
}, props.delayTime);
});
}
// 获取底部滑块的位移距离 type为1版本
function getLine1(index) {
let moveX = 0;
let tabsLineWidth = props.tabsLineStyle.width;
for (let i = 0; i < tabInfo.length; i++) {
if (i <= index) {
let ele = tabInfo[i];
let tWidth = '';
if (tabsLineWidth) {
let tabsLineWidthRes = extractNumberAndUnit(tabsLineWidth);
switch (tabsLineWidthRes.unit) {
case 'rpx':
tWidth = tabsLineWidthRes.number / 2;
break;
case 'px':
tWidth = tabsLineWidthRes.number;
break;
}
} else {
tWidth = ele.width / 2;
}
if (index == i) {
tabsLineWidth = tWidth;
}
if (i < index) {
moveX += ele.width;
} else {
moveX += (ele.width - tWidth) / 2;
}
} else {
break;
}
}
tabsLineInfo.push({
width: tabsLineWidth + 'px',
transform: `translateX(${moveX}px)`,
moveX: moveX,
});
}
// 获取底部滑块的位移距离 type为2版本
function getLine2(index) {
let moveX = 0;
let tabsLineWidth = props.tabsLineStyle.width;
let tWidth = '';
let ele = tabInfo[index];
if (tabsLineWidth) {
let tabsLineWidthRes = extractNumberAndUnit(tabsLineWidth);
switch (tabsLineWidthRes.unit) {
case 'rpx':
tWidth = tabsLineWidthRes.number / 2;
break;
case 'px':
tWidth = tabsLineWidthRes.number;
break;
}
} else {
tWidth = ele.width / 2;
}
moveX = ele.left - tabsBoxInfo.left + (ele.width - tWidth) / 2;
tabsLineInfo.push({
width: tWidth + 'px',
transform: `translateX(${moveX}px)`,
moveX: moveX,
});
}
</script>
<style lang="scss" scoped>
.tabs-box {
position: relative;
.init-box {
position: absolute;
z-index: 999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #e6ebff;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
color: #fff;
}
}
.tabs {
display: flex;
position: relative;
}
.tabs-line {
position: absolute;
left: 0;
transition: all 0.2s;
}
.tab {
position: relative;
white-space: nowrap;
.tab-line {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.tab-line-test {
position: absolute;
left: 50%;
bottom: 0;
width: 50%;
transform: translateX(-50%);
background: #000;
height: 4rpx;
}
.badge-box {
position: absolute;
right: 0;
top: 0;
transform: translate(30%, 50%);
padding: 6rpx;
background: #f56c6c;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 12rpx;
color: #fff;
}
}
</style>
页面使用
<myTabs type="1" :tabList="tabList" :activeTab="activeTab" @changeTab="changeTab"></myTabs>
import myTabs from './myTabs.vue';
let tabList = ref([
{
id: 1,
name: '首页',
},
{
id: 2,
name: '定制',
},
{
id: 3,
name: '视频',
},
{
id: 4,
name: '公告',
},
]);
let activeTab = ref(1);
function changeTab(tab) {
activeTab.value = tab.id;
}