本文介绍了一个通用的导航栏组件实现方案,支持点击切换和滑动切换功能。

主要特点包括:

1) 当前tab会自动居中显示;

2) 支持上拉刷新和下拉加载;

3) 采用Vue3+setup语法封装。

组件分为顶部导航栏和内容区两部分,导航栏使用scroll-view实现横向滚动,内容区使用swiper实现滑动切换。组件通过计算tab位置实现居中效果,并提供了丰富的插槽和事件接口。

示例代码展示了如何在订单管理页面中使用该组件,支持不同订单状态的切换和分页加载。

该实现方案简洁高效,适合各类需要tab切换的场景。

以下就是封装的内容

<template>
	<view class="category-swiper">
		<!-- 1. 分类栏 -->
		<scroll-view ref="tabScroll" scroll-x :scroll-left="tabScrollLeft" scroll-with-animation class="tab-scroll">
			<view class="tab-list">
				<view v-for="(item, idx) in list" :key="idx" :class="[
            'tab-item',
            { active: current === idx }
          ]" @tap="changeTab(idx)">
					<!-- 插槽:category -->
					<slot name="category" :item="item" :index="idx" :active="current === idx">
						<!-- 默认样式 -->
						<text>{{ item.title || item.name || item }}</text>
					</slot>
				</view>
			</view>
		</scroll-view>

		<!-- 2. 内容区 -->
		<swiper class="content-swiper" :style="`height:${contentHeight}`" :current="current" @change="swiperChange"
			@animationfinish="animationFinish" :duration="500">
			<swiper-item v-for="(item, idx) in list" :key="idx">
				<!-- 竖向 scroll-view,允许内容超出滚动 -->
				<scroll-view scroll-y class="content-scroll" :show-scrollbar="false" enhanced bounces="false"
					:refresher-enabled="refresher" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh"
					@refresherrestore="onRestore" @scrolltolower="emit('scrolltolower',item)">
					<!-- 插槽:content -->
					<slot name="content" :item="item" :index="idx" :active="current === idx">
						<!-- 默认样式 -->
						<view style="margin-top: 32rpx;">
						</view>
					</slot>
				</scroll-view>
			</swiper-item>
		</swiper>
	</view>
</template>
<script lang="ts" setup>
	import { computed, getCurrentInstance, nextTick, ref, watch, onMounted } from 'vue'


	/* ① 先声明,不立即实例化 */
	let globalDate : ReturnType<typeof globalInfoStore>
	const isRefreshing = ref(false)  // 控制刷新状态 =

	// 刷新
	const onRefresh = async () => {
		isRefreshing.value = true
		try {
			await emit('refresherData')
			// 等待 DOM 更新完成再关闭动画
			await nextTick()
			// 加微小延时确保 scroll-view 接收到状态变更
			setTimeout(() => {
				isRefreshing.value = false
			}, 100)
		} catch (e) {
			isRefreshing.value = false
		}
	}

	// 复位完成(动画收起后触发)
	const onRestore = () => {
		console.log('刷新已复位')
		isRefreshing.value = false  // 确保状态同步
	}

	const proxy = getCurrentInstance().proxy
	/* emit */
	const emit = defineEmits(['update:modelValue', 'change', 'scrolltolower', 'handleContentItem', 'refresherData'])
	/* props */
	const props = defineProps({
		list: {
			type: Array,
			required: true,
			default: () => [],
		},
		modelValue: {
			type: Number,
			default: 0
		},
		// 内容高度
		contentHeight: {
			type: String,
			default: 'calc(100vh - 120rpx)'
		},
        // 是否开启自定义刷新
		refresher: {
			type: Boolean,
			default: false
		}
	})


	/* 当前索引 */
	const current = ref(props.modelValue)

	watch(
		() => props.modelValue,
		v => (current.value = v)
	)

	watch(current, v => {
		emit('update:modelValue', v)
		emit('change', v)
	})

	/* 切换 tab */
	const changeTab = idx => {
		if (current.value == idx || isAnimating.value) return
		isAnimating.value = true
		current.value = idx
	}

	/* swiper 滑动改变 */
	const swiperChange = e => {
		const idx = e.detail.current
		if (current.value != idx) {
			isAnimating.value = true
			current.value = idx
		}
	}

	/* 每个 tab 的宽度(rpx -> px) */
	const tabScrollLeft = ref(0)
	const tabRects = ref([])   // 每个 tab 的 left / width
	const isAnimating = ref(false)
    const windowWidth = ref(750)

	const queryTabs = () => {
		if (props.list.length == 0) {
			setTimeout(() => { queryTabs() }, 500)
			return
		}
		uni.createSelectorQuery()
			.in(proxy)             // <script setup> 里用 getCurrentInstance().proxy
			.selectAll('.tab-item')
			.boundingClientRect()
			.exec(([rects]) => {
				tabRects.value = rects || []
				calcScrollLeft(current.value)
			})
	}

	/* 计算 scrollLeft 让当前 tab 居中 */
	const calcScrollLeft = idx => {
		const rect = tabRects.value[idx]
		if (!rect) return
		tabScrollLeft.value = rect.left + rect.width / 2 - windowWidth.value / 2
	}


	/* swiper 滑动结束后立即居中 */
	const animationFinish = async e => {
		const idx = e.detail.current
		await nextTick(() => calcScrollLeft(idx))
		isAnimating.value = false
	}

	/* 初始化 & 外部 v-model 变化时也居中 */
	watch(current, idx => calcScrollLeft(idx), { immediate: true })

	/* 初始化 */
	calcScrollLeft(current.value)

	onMounted(() => {
		const systemInfo = uni.getSystemInfoSync()
        windowWidth.value = systemInfo.windowWidth
		queryTabs()
	})
<style lang="less" scoped>
	.category-swiper {
		display: flex;
		flex-direction: column;
		height: 100%;
	}

	/* 分类栏 */
	.tab-scroll {
		white-space: nowrap;
		height: 80rpx;
		position: sticky;
		top: 0;
		left: 0;
		z-index: 9;
	}

	.tab-list {
		width: fit-content;
		height: 100%;
		display: flex;
		align-items: center;
		gap: 0 32rpx;
	}

	.tab-item {
		flex-shrink: 0;
		text-align: center;
		font-size: 28rpx;
		color: #333;
		line-height: 80rpx;
		// background: #f4f4f4;
	}

	.tab-item.active {
		color: #CFA972;
		font-weight: bold;
		position: relative;
	}

	.tab-item.active::after {
		content: '';
		position: absolute;
		bottom: 0;
		left: 50%;
		transform: translateX(-50%);
		width: 40rpx;
		height: 4rpx;
		background: #CFA972;
		border-radius: 2rpx;
	}

	/* 内容区 */
	.content-swiper {
		padding-bottom: 40rpx;
	}

	.content-scroll {
		height: 100%;
		/* scroll-view 占满 swiper-item */
	}

	/* H5 / App-V3 保险写法 */
	.content-scroll ::-webkit-scrollbar {
		display: none;
		width: 0;
		height: 0;
		color: transparent;
	}

	.tab-scroll ::-webkit-scrollbar {
		display: none;
		width: 0;
		height: 0;
		color: transparent;
	}

在页面中使用

<view class="page-content">
		<category-swiper :list="list" v-model="activeIndex" :refresher="true" @change="changeTab" @scrolltolower="getMore"
			@refresherData="getOrderList(true)">
			<!-- 内容区 -->
			<template #content="{ item }">
				<view style="margin-top: 40rpx;" v-if="item.orderList.length > 0">
				</view>
			</template>
		</category-swiper>
	</view>
</template>
<script lang="ts" setup>
	import { ref } from 'vue'
	import { onLoad, onUnload } from "@dcloudio/uni-app"

    const activeIndex = ref(0)
	const isLoading = ref(false)
	const param = ref({
		userId: uni.getStorageSync('userId'),
		version: '3.0',
		orderCode: ''
	})
	const list = ref([
		{
			title: '全部',
			orderStatus: '',
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '待付款',
			orderStatus: 1,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '待发货',
			orderStatus: 2,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '待收货',
			orderStatus: 3,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '待评价',
			orderStatus: 4,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '服务订单',
			orderStatus: -200,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		},
		{
			title: '退款/售后',
			orderStatus: -100,
			pageNo: 1,
			pageTotal: 1,
			orderList: [],
			_loaded: false,
		}
	])

    // 切换状态
	const changeTab = (index) => {
		activeIndex.value = index
		const currentTab = list.value[index];
		// 如果还没加载过,才请求
		if (!currentTab._loaded) {
			currentTab.pageNo = 1;
			getOrderList(); // 你的请求方法
			currentTab._loaded = true; // 标记已加载
		}
	}

    // 获取更多
	const getMore = (item) => {
		if (item.pageNo >= item.pageTotal) return
		const data = list.value.find(order => order.orderStatus === item.orderStatus)
		data.pageNo += 1
		getOrderList()
	}
</script>

如果有什么更好的封装方法,希望大家一起讨论学习,以上只是个人在项目中自己封装使用的简单tab

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐