上一节我们为项目配置了基于 airbnb 代码风格的 Eslint,今天我们来开发一个可滑动的带动效的 Tabs 标签组件;今天的开发将继续在前两节配置的项目中进行,建议小伙伴们先看完前面两节内容,可通过合集进入查看。
1、最终效果及思路分析
当标签过多时,我们需要能够左右滑动,这里首先考虑使用 scroll-view,让 scroll-view 在x轴方向上允许滑动,
在 scroll-view 的内部用一个 view 作为父节点, 在这个父节点内部需要两个 view 子节点,一个用来包裹标签,
另一个使用绝对定位来做下边的红色条块,当点击某一个标签时,滑块会滑动到该标签下方,同时在合适的时机需要将前面或者后面隐藏的标签拉出来;
要使条块滑动到点击标签的下方,我们需要在点击时获取到当前标签的位置,并把它在屏幕上 left 的值赋值给条块,同时每个标签的长度可能不一致,
那么还需要将当前标签的 width 赋值给条块;scroll-view 有一个 scroll-into-view 属性,官网介绍如下:
我们可以使用这个属性将隐藏的标签拉出来
2、组件开发
今天我们使用 uni_modules 插件形式来开发,uni_modules 插件的创建方式参照官网文档,根据1中的布局思路先上布局
<template>
<scroll-view scroll-x class="quick-tabs">
<view class="quick-tabs-wrap">
<view>
<view class="quick-tab-item">
<text>标签文字</text>
</view>
</view>
<view class="quick-tabs-block" />
</view>
</scroll-view>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
.quick-tabs{
width: 100%;
white-space: nowrap;
::-webkit-scrollbar{
width: 0;
height: 0;
color: transparent;
}
.quick-tabs-wrap{
display: inline-block;
position: relative;
.quick-tab-item{
display: inline-block;
margin-right: 32rpx;
padding: 16rpx 0 32rpx;
&:last-child{
margin-right: 0;
}
}
}
.quick-tabs-block{
position: absolute;
left: 0;
bottom: 0;
height: 16rpx;
}
}
</style>
标签数据肯定是从外部传递进来的,同时我们点击了标签时需要让外部知道当前是哪一个标签,下面我们先定义以下参数:
<script lang="ts" setup>
type Props = {
moduleValue: number // 外部使用v-model来绑定当前点击的标签索引
tabs: string[] // 标签数组数据
color: string // 标签文字的默认颜色
activeColor: string // 标签激活时的颜色,同时也是滑动条块的颜色
fontSize: number // 标签文字大小
}
const props = withDefaults(defineProps<Props>({
modelValue: 0,
color: '#606266',
activeColor: '#DE3F3F',
fontSize: 30,
}))
</script>
使用 uni.createSelectorQuery() 来获取 dom 信息,使用这个 api 需要先为所有标签节点指定一个id,id必须是唯一的,所以组件中父节点以及子节点的id我们都动态生成,
同时拿到的标签节点的 left 位置是基于屏幕的,我们还需要为父节点指定id,通过标签节点的 left 减去父节点的 left 值,就得到了标签节点基于父节点的 left 值,
通过下面的代码来完成这一操作:
<scroll-view
scroll-x
class="quick-tabs">
<view :id="parentNodeId" class="quick-tabs-wrap">
<view class="quick-tabs-container">
<view
v-for="(item, index) of tabList"
:key="index"
:id="item.id"
class="quick-tab-item">
<text>{{ item.label }}</text>
</view>
</view>
<view class="quick-tabs-block" />
</view>
</scroll-view>
<script lang="ts" setup>
type Props = {
moduleValue: number // 外部使用v-model来绑定当前点击的标签索引
tabs: string[] // 标签数组数据
color: string // 标签文字的默认颜色
activeColor: string // 标签激活时的颜色,同时也是滑动条块的颜色
fontSize: number // 标签文字大小
}
const { proxy } = getCurrentInstance() as Record<string, any>
const props = withDefaults(defineProps<Props>({
modelValue: 0,
color: '#606266',
activeColor: '#DE3F3F',
fontSize: 30,
}))
const { modelValue, tabs } = toRefs(props)
const tabList = computed(() => {
return tabs.value.map((item) => ({
label: item,
id: `quick-tab-${Math.ceil(Math.random() * 10e5).toString(36)}`
}))
})
const domQuery = uni.createSelectorQuery().in(proxy)
const parentNode = ref<Record<string, any>>({})
const currentTabNode = ref<Record<string, any>>({})
const parentNodeId = `quick-${Math.ceil(Math.random() * 10e5).toString(36)}`
const currentTabNodeId = ref(tabList.value[modelValue.value].id)
</script>
上面的代码中我们声明了两个变量 parentNode 和 currentTabNode,分别用于存放父节点信息和当前的子节点信息(点击的那一个),下面我们实现一个获取节点数据的方法,
并实现标签节点的点击事件:
<template>
<scroll-view
scroll-x
class="quick-tabs">
<view :id="parentNodeId" class="quick-tabs-wrap">
<view class="quick-tabs-container">
<view
v-for="(item, index) of tabList"
:key="index"
:id="item.id"
class="quick-tab-item"
@click="click(index, item.id)">
<text>{{ item.label }}</text>
</view>
</view>
<view class="quick-tabs-block" :style="tabBlockStyle" />
</view>
</scroll-view>
</template>
<script lang="ts" setup>
type Props = {
modelValue: number // 外部使用v-model来绑定当前点击的标签索引
tabs: string[] // 标签数组数据
color: string // 标签文字的默认颜色
activeColor: string // 标签激活时的颜色,同时也是滑动条块的颜色
fontSize: number // 标签文字大小
}
const { proxy } = getCurrentInstance() as Record<string, any>
const emits = defineEmits(['update:modelValue', 'change'])
const props = withDefaults(defineProps<Props>({
modelValue: 0,
color: '#606266',
activeColor: '#DE3F3F',
fontSize: 30,
}))
const { modelValue, tabs, activeColor } = toRefs(props)
const tabList = computed(() => {
return tabs.value.map((item) => ({
label: item,
id: `quick-tab-${Math.ceil(Math.random() * 10e5).toString(36)}`
}))
})
const domQuery = uni.createSelectorQuery().in(proxy)
const parentNode = ref<Record<string, any>>({})
const currentTabNode = ref<Record<string, any>>({})
const parentNodeId = `quick-${Math.ceil(Math.random() * 10e5).toString(36)}`
const currentTabNodeId = ref(tabList.value[modelValue.value].id) // 默认为第一个子节点的id
const tabBlockStyle = computed(() => {
const { left: parentNodeLeft } = parentNode.value
const { left: currentTabLeft, width } = currentTabNode.value
return {
left: currentTabLeft ? `${currentTabLeft - parentNodeLeft}px` : 0,
width: `${width}px`,
backgroundColor: activeColor.value,
}
})
const getDomNodeInfo = (domId: string, callback: (data: Record<string, any>) => void) => {
domQuery.select(`#${domId}`).boundingClientRect(callback).exec()
}
const click = (index: number, domId: string) => {
getDomNodeInfo(domId, (data) => currentTabNode.value = data)
emits('update:modelValue', index)
emits('change', index)
}
onLoad(() => {
getDomNodeInfo(parentNodeId, (data) => parentNode.value = data)
getDomNodeInfo(tabList.value[modelValue.value].id, (data) => currentTabNode.value = data)
})
</script>
上面的代码中我们实现了:当页面加载的时候获取父节点信息,以及当前的标签节点信息,现在,当点击标签的时候,条块就会移动到标签下方,但是还没有滑动效果,
我们在 css 中为它添加 transition 属性就可以了:
<style lang="scss" scoped>
.quick-tabs{
width: 100%;
white-space: nowrap;
::-webkit-scrollbar{
width: 0;
height: 0;
color: transparent;
}
.quick-tabs-wrap{
display: inline-block;
position: relative;
.quick-tab-item{
display: inline-block;
margin-right: 32rpx;
padding: 16rpx 0 32rpx;
&:last-child{
margin-right: 0;
}
}
}
.quick-tabs-block{
position: absolute;
left: 0;
bottom: 0;
height: 16rpx;
transition: all 200ms; // 加上它
}
}
</style>
到这里,组件已经完成了大半部分了,接下来我们为标签添加选中样式,以及实现将隐藏的标签拉出来的功能:
定义一个 computed 属性,并将其添加到滑动条块上
<template>
<scroll-view
scroll-x
:scroll-into-view="currentTabNodeId"
:show-scrollbar="false"
scroll-with-animation
class="quick-tabs">
<view :id="parentNodeId" class="quick-tabs-wrap">
<view class="quick-tabs-container">
<view
v-for="(item, index) of tabList"
:key="index"
:id="item.id"
class="quick-tab-item"
@click="click(index, item.id)">
<text :style="tabTextStyle(index)">{{ item.label }}</text>
</view>
</view>
<view class="quick-tabs-block" :style="tabBlockStyle" />
</view>
</scroll-view>
</template>
<script lang="ts" setup>
const tabTextStyle = computed(() => (index: number) => (
{
fontSize: `${fontSize.value}rpx`,
color: index === modelValue.value ? activeColor.value : color.value,
fontWeight: index === modelValue.value ? 'bold' : '',
}
))
</script>
监听 modelValue 变化,设置 currentTabNodeId 的值,这样在点击标签的时候就可以实现将隐藏的标签拉出来了,同时给 scroll-view 加上 scroll-with-animation 属性,
标签拉出来的过程中就有了一些过度动画,不会显得那么生硬。
<script lang="ts" setup>
const setScrollViewId = (newValue: number) => {
const maxIndex = tabList.value.length - 1
if (newValue <= maxIndex && newValue >= 1) {
currentTabNodeId.value = tabList.value[newValue - 1].id
}
}
watch(() => modelValue.value, setScrollViewId)
</script>
在页面中使用
<!-- pages/index/index -->
<template>
<quick-tabs v-model="current" :tabs="tabs" />
</template>
<script lang="ts" setup>
const current = ref(0)
const tabs = [
'点赞关注',
'评论分享',
'新闻资讯',
'影音视听',
'人文地理',
'社会百科',
]
</script>
<style>
</style>
```typescript
下面是完整代码:
```typescript
<template>
<scroll-view
scroll-x
:scroll-into-view="currentTabNodeId"
:show-scrollbar="false"
scroll-with-animation
class="quick-tabs">
<view :id="parentNodeId" class="quick-tabs-wrap">
<view class="quick-tabs-container">
<view
v-for="(item, index) of tabList"
:key="index"
:id="item.id"
class="quick-tab-item"
@click="click(index, item.id)">
<text :style="tabTextStyle(index)">{{ item.label }}</text>
</view>
</view>
<view class="quick-tabs-block" :style="tabBlockStyle" />
</view>
</scroll-view>
</template>
<script lang="ts" setup>
type Props = {
modelValue?: number
tabs: string[]
fontSize?: number
color?: string
activeColor?: string
}
const { proxy } = getCurrentInstance() as Record<string, any>
const emits = defineEmits(['update:modelValue', 'change'])
const props = withDefaults(defineProps<Props>(), {
modelValue: 0,
color: '#606266',
activeColor: '#DE3F3F',
fontSize: 30,
})
const { modelValue, tabs, color, activeColor, fontSize } = toRefs(props)
const domQuery = uni.createSelectorQuery().in(proxy)
const parentNodeId = `quick-${Math.ceil(Math.random() * 10e5).toString(36)}`
const parentNode = ref<Record<string, any>>({})
const currentTabNode = ref<Record<string, any>>({})
const tabList = computed(() => {
return tabs.value.map((item) => ({
label: item,
id: `quick-tab-${Math.ceil(Math.random() * 10e5).toString(36)}`
}))
})
const currentTabNodeId = ref(tabList.value[modelValue.value].id)
const tabTextStyle = computed(() => (index: number) => (
{
fontSize: `${fontSize.value}rpx`,
color: index === modelValue.value ? activeColor.value : color.value,
fontWeight: index === modelValue.value ? 'bold' : '',
}
))
const tabBlockStyle = computed(() => {
const { left: parentNodeLeft } = parentNode.value
const { left: currentTabLeft, width } = currentTabNode.value
return {
left: currentTabLeft ? `${currentTabLeft - parentNodeLeft}px` : 0,
width: `${width}px`,
backgroundColor: activeColor.value,
}
})
const getDomNodeInfo = (domId: string, callback: (data: Record<string, any>) => void) => {
domQuery.select(`#${domId}`).boundingClientRect(callback).exec()
}
const click = (index: number, domId: string) => {
getDomNodeInfo(domId, (data) => currentTabNode.value = data)
emits('update:modelValue', index)
emits('change', index)
}
const setScrollViewId = (newValue: number) => {
const maxIndex = tabList.value.length - 1
if (newValue <= maxIndex && newValue >= 1) {
currentTabNodeId.value = tabList.value[newValue - 1].id
}
}
watch(() => modelValue.value, setScrollViewId)
onLoad(() => {
getDomNodeInfo(parentNodeId, (data) => parentNode.value = data)
getDomNodeInfo(tabList.value[modelValue.value].id, (data) => currentTabNode.value = data)
})
</script>
<style lang="scss">
.quick-tabs{
width: 100%;
white-space: nowrap;
::-webkit-scrollbar{
width: 0;
height: 0;
color: transparent;
}
.quick-tabs-wrap{
display: inline-block;
position: relative;
.quick-tab-item{
display: inline-block;
margin-right: 32rpx;
padding: 16rpx 0 32rpx;
&:last-child{
margin-right: 0;
}
}
}
.quick-tabs-block{
position: absolute;
left: 0;
bottom: 0;
height: 16rpx;
transition: all 200ms;
}
}
</style>