三、在uni-app中开发一个带滑块的Tabs标签组件

上一节我们为项目配置了基于 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>
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热爱生活的正道的光

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值