uniapp 封装一个导航栏,可切换tab也可滑动切换
·
本文介绍了一个通用的导航栏组件实现方案,支持点击切换和滑动切换功能。
主要特点包括:
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
更多推荐
所有评论(0)