uniapp 封装tabs栏

1,013 阅读5分钟

封装缘由

项目要求让tabs的元素居中展示,但是tabs数组中的数据又比较少,而UI组件库(例如uview)的tabs组件是没有居中展示的功能的

效果

type为1时

image.png

type为2时

image.png

参数

组件属性说明示例
typetabs组件内容展示的对齐方式字符串:1 tabs内容很多,超过屏幕宽度、2 tabs内容很少,不超过屏幕宽度,tabs内容居中显示
tabListtab数据列表数组
activeTab当前激活的tab的IdNumber数字
isStickytabs组件是否粘性定位Boolean
tabsBoxStyle自定义tabs组件的样式Object
tabStyle自定义tab样式Object
tabActiveStyle自定义tab激活样式Object
tabsLineStyle自定义底部滑块样式Object
tabLineImg底部滑块图片地址(网络图片)String
badgeShow徽标是否显示Boolean
badgeStyle自定义徽标样式Object
delayTime延迟时间,单位:msNumber数字
proxyField代理字段Object

参数解释

proxyField:

用于指定组件中指定字段对应的是tabList中哪个字段

数据格式如下:

image.png

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;
	}