Vue3仿ant-design的Segmented分段组件

252 阅读2分钟

实现带滑动效果的 Tab 切换栏的示例,类似 ant-design 的 Segmented 组件效果:

<!-- TabSegmented.vue -->
<template>
  <div class="tab-segmented" ref="tabContainer">
    <!-- 选项容器 -->
    <div class="tab-wrapper">
      <div
        v-for="(item, index) in options"
        :key="index"
        class="tab-item"
        :class="{ active: modelValue === item.value }"
        @click="handleSelect(item)"
        ref="tabItems"
      >
        {{ item.label }}
      </div>
      <!-- 滑块 -->
      <div
        class="slider"
        :style="{
          width: sliderWidth + 'px',
          transform: `translateX(${sliderOffset}px)`,
        }"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'

interface Option {
  label: string
  value: string | number
}

const props = defineProps<{
  options: Option[]
  modelValue: string | number
}>()

const emit = defineEmits(['update:modelValue'])

const tabContainer = ref<HTMLElement | null>(null)
const tabItems = ref<HTMLElement[]>([])
const sliderWidth = ref(0)
const sliderOffset = ref(0)

// 更新滑块位置和宽度
const updateSlider = (index: number) => {
  if (tabItems.value[index]) {
    const currentTab = tabItems.value[index]
    sliderWidth.value = currentTab.offsetWidth
    sliderOffset.value = currentTab.offsetLeft
  }
}

// 处理选择
const handleSelect = (item: Option) => {
  emit('update:modelValue', item.value)
}

// 监听选中值变化
watch(
  () => props.modelValue,
  () => {
    const index = props.options.findIndex(item => item.value === props.modelValue)
    if (index !== -1) {
      updateSlider(index)
    }
  }
)

// 组件挂载后初始化滑块位置
onMounted(() => {
  const index = props.options.findIndex(item => item.value === props.modelValue)
  if (index !== -1) {
    updateSlider(index)
  }
})
</script>

<style scoped>
.tab-segmented {
  width: 100%;
  padding: 4px;
  background-color: #f0f0f0;
  border-radius: 6px;
}

.tab-wrapper {
  position: relative;
  display: flex;
  width: 100%;
}

.tab-item {
  flex: 1;
  padding: 6px 12px;
  text-align: center;
  cursor: pointer;
  transition: color 0.3s;
  position: relative;
  z-index: 1;
  user-select: none;
}

.tab-item.active {
  color: #1890ff;
}

.slider {
  position: absolute;
  height: 100%;
  background-color: #fff;
  border-radius: 4px;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  top: 0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
</style>

使用示例:

<!-- App.vue -->
<template>
  <div class="container">
    <TabSegmented
      v-model="activeTab"
      :options="options"
    />
    
    <!-- 展示当前选中的值 -->
    <div class="content">
      当前选中: {{ activeTab }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TabSegmented from './components/TabSegmented.vue'

const activeTab = ref('1')

const options = [
  { label: '选项一', value: '1' },
  { label: '选项二', value: '2' },
  { label: '选项三', value: '3' },
  { label: '选项四', value: '4' },
]
</script>

<style scoped>
.container {
  max-width: 600px;
  margin: 100px auto;
  padding: 20px;
}

.content {
  margin-top: 20px;
  text-align: center;
}
</style>

这个实现包含以下特性:

  1. 支持 v-model 双向绑定
  2. 平滑的滑块过渡动画
  3. 响应式布局
  4. TypeScript 类型支持
  5. 可自定义选项数量

如果你想要添加更多功能,可以考虑以下扩展:

  1. 添加禁用状态:
<template>
  <div
    class="tab-item"
    :class="{
      active: modelValue === item.value,
      disabled: item.disabled
    }"
    @click="!item.disabled && handleSelect(item)"
  >
    {{ item.label }}
  </div>
</template>

<style>
.tab-item.disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
</style>
  1. 添加图标支持:
<template>
  <div class="tab-item">
    <i v-if="item.icon" :class="item.icon"></i>
    <span>{{ item.label }}</span>
  </div>
</template>
  1. 添加自定义颜色支持:
<script setup lang="ts">
const props = defineProps<{
  options: Option[]
  modelValue: string | number
  activeColor?: string
  backgroundColor?: string
}>()
</script>

<template>
  <div 
    class="tab-segmented"
    :style="{
      backgroundColor: backgroundColor
    }"
  >
    <!-- ... -->
    <div
      class="tab-item"
      :style="{
        color: modelValue === item.value ? activeColor : ''
      }"
    >
      {{ item.label }}
    </div>
  </div>
</template>
  1. 添加尺寸支持:
<script setup lang="ts">
const props = defineProps<{
  size?: 'small' | 'medium' | 'large'
}>()
</script>

<style scoped>
.tab-segmented.small { padding: 2px; }
.tab-segmented.medium { padding: 4px; }
.tab-segmented.large { padding: 6px; }

.tab-item.small { padding: 4px 8px; }
.tab-item.medium { padding: 6px 12px; }
.tab-item.large { padding: 8px 16px; }
</style>
  1. 添加动画效果:
.tab-item {
  transition: all 0.3s;
}

.tab-item:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

.slider {
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}

这些扩展可以根据实际需求选择性添加。组件的核心是通过监听选中值的变化来更新滑块的位置,并使用 CSS transition 实现平滑的过渡效果。