Vue3分段控制器(Segmented)

简介: 这是一个基于 Vue 的分段控制器组件 `Segmented`,支持多种选项和自定义渲染。通过 `v-model` 绑定当前选中值,并提供 `block`、`disabled` 和 `size` 等属性来调整样式。

效果如下图:在线预览

在这里插入图片描述
在这里插入图片描述

APIs

Segmented

参数 说明 类型 默认值
block 是否将宽度调整为父元素宽度,同时所有选项占据相同的宽度 boolean false
disabled 是否禁用 boolean false
options 选项数据 string[] | number[] | SegmentedOption[] []
size 控件尺寸 ‘small’ | ‘middle’| ‘large’ ‘middle’
value v-model 当前选中的值 string | number undefined

SegmentedOption Type

名称 说明 类型 默认值
label? 选项名 string undefined
value 选项值 string | number undefined
disabled? 是否禁用选项 boolean false
payload? 自定义数据载体 any undefined

Events

名称 说明 类型
change 选项变化时的回调函数 (value: string number) => void

创建分段控制器组件Segmented.vue

<script setup lang="ts">
interface SegmentedOption {
  label?: string // 选项名
  value: string | number // 选项值
  disabled?: boolean // 是否禁用选项
  payload?: any // 自定义数据载体
}
interface Props {
  block?: boolean // 是否将宽度调整为父元素宽度,同时所有选项占据相同的宽度
  disabled?: boolean // 是否禁用
  options?: string[] | number[] | SegmentedOption[] // 选项数据
  size?: 'small' | 'middle' | 'large' // 控件尺寸
  value?: string | number // (v-model) 当前选中的值
}
const props = withDefaults(defineProps<Props>(), {
  block: false,
  disabled: false,
  options: () => [],
  size: 'middle',
  value: undefined
})
const emits = defineEmits(['update:value', 'change'])
function onSelected(value: string | number) {
  if (value !== props.value) {
    emits('update:value', value)
    emits('change', value)
  }
}
function getOptionDisabled(option: string | number | SegmentedOption) {
  if (typeof option == 'object') {
    return option?.disabled || false
  }
  return false
}
function getOptionValue(option: string | number | SegmentedOption) {
  if (typeof option == 'object') {
    return option.value
  }
  return option
}
function getOptionLabel(option: string | number | SegmentedOption) {
  if (typeof option == 'object') {
    return option.label
  }
  return option
}
</script>
<template>
  <div
    class="m-segmented"
    :class="{
      'segmented-small': size == 'small',
      'segmented-large': size == 'large',
      'segmented-block': block
    }"
  >
    <div class="m-segmented-group">
      <div
        class="m-segmented-item"
        :class="{
          'segmented-item-selected': value === getOptionValue(option),
          'segmented-item-disabled': disabled || getOptionDisabled(option),
          'segmented-item-block': block
        }"
        v-for="(option, index) in options"
        :key="index"
        @click="disabled || getOptionDisabled(option) ? () => false : onSelected(getOptionValue(option))"
      >
        <input
          type="radio"
          class="segmented-item-input"
          :checked="value === getOptionValue(option)"
          :disabled="disabled || getOptionDisabled(option)"
        />
        <div
          class="segmented-item-label"
          :title="typeof option === 'object' && option.payload ? undefined : String(getOptionLabel(option))"
        >
          <slot
            name="label"
            :label="getOptionLabel(option)"
            :payload="typeof option === 'object' ? option.payload : {}"
          >
            {
  { getOptionLabel(option) }}
          </slot>
        </div>
      </div>
    </div>
  </div>
</template>
<style lang="less" scoped>
.m-segmented {
  display: inline-block;
  padding: 2px;
  color: rgba(0, 0, 0, 0.65);
  font-size: 14px;
  line-height: 1.5714285714285714;
  background-color: #f5f5f5;
  border-radius: 6px;
  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  .m-segmented-group {
    position: relative;
    display: flex;
    align-items: stretch;
    justify-items: flex-start;
    width: 100%;
    .m-segmented-item {
      position: relative;
      text-align: center;
      cursor: pointer;
      transition:
        color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1),
        background-color 0.2s;
      border-radius: 4px;
      &:hover:not(.segmented-item-selected):not(.segmented-item-disabled) {
        color: rgba(0, 0, 0, 0.88);
        &::after {
          background-color: rgba(0, 0, 0, 0.06);
        }
      }
      &::after {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        inset-inline-start: 0;
        border-radius: inherit;
        transition: background-color 0.2s;
        pointer-events: none;
        content: '';
      }
      .segmented-item-input {
        position: absolute;
        inset-block-start: 0;
        inset-inline-start: 0;
        width: 0;
        height: 0;
        opacity: 0;
        pointer-events: none;
      }
      .segmented-item-label {
        min-height: 28px;
        line-height: 28px;
        padding: 0 11px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
    }
    .segmented-item-selected {
      background-color: #ffffff;
      box-shadow:
        0 1px 2px 0 rgba(0, 0, 0, 0.03),
        0 1px 6px -1px rgba(0, 0, 0, 0.02),
        0 2px 4px 0 rgba(0, 0, 0, 0.02);
      color: rgba(0, 0, 0, 0.88);
    }
    .segmented-item-disabled {
      color: rgba(0, 0, 0, 0.25);
      cursor: not-allowed;
    }
  }
}
.segmented-small {
  border-radius: 4px;
  .m-segmented-group .m-segmented-item {
    border-radius: 2px;
    .segmented-item-label {
      min-height: 20px;
      line-height: 20px;
      padding: 0 7px;
    }
  }
}
.segmented-large {
  border-radius: 8px;
  .m-segmented-group .m-segmented-item {
    border-radius: 6px;
    .segmented-item-label {
      min-height: 36px;
      line-height: 36px;
      padding: 0 11px;
      font-size: 16px;
    }
  }
}
.segmented-block {
  display: flex;
  width: 100%;
  .m-segmented-group .m-segmented-item {
    flex: 1;
    min-width: 0;
  }
}
</style>
AI 代码解读

在要使用的页面引入

<script setup lang="ts">
import Segmented from './Segmented.vue'
import { reactive, ref } from 'vue'
const options = reactive(['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly'])
const optionsDisabled = reactive([
  'Daily',
  { label: 'Weekly', value: 'Weekly', disabled: true },
  'Monthly',
  { label: 'Quarterly', value: 'Quarterly', disabled: true },
  'Yearly'
])
const value = ref(options[0])
const value2 = ref('Daily')
const onChange = (value: string | number) => {
  console.log('change', value)
}
const dynamicOptions = reactive(['Daily', 'Weekly', 'Monthly'])
const dynamicValue = ref(dynamicOptions[0])
const loading = ref(false)
const disabled = ref(false)
const loadMore = () => {
  loading.value = true
  setTimeout(() => {
    dynamicOptions.push(...['Quarterly', 'Yearly'])
    loading.value = false
    disabled.value = true
  }, 1000)
}
const customOptions1 = reactive([
  {
    label: 'user1',
    value: 'user1',
    payload: {
      src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.5/1.jpg',
      style: { backgroundColor: '#f56a00' }
    }
  },
  {
    label: 'user2',
    value: 'user2',
    payload: {
      style: { backgroundColor: '#f56a00' },
      content: 'K'
    }
  },
  {
    label: 'user3',
    value: 'user3',
    payload: {
      icon: 'User',
      style: { backgroundColor: '#f56a00' }
    }
  }
])
const customValue = ref(customOptions1[0].value)
const customOptions2 = reactive([
  {
    value: 'spring',
    payload: {
      title: 'Spring',
      subTitle: 'Jan-Mar'
    }
  },
  {
    value: 'summer',
    payload: {
      title: 'Summer',
      subTitle: 'Apr-Jun'
    }
  },
  {
    value: 'autumn',
    payload: {
      title: 'Autumn',
      subTitle: 'Jul-Sept'
    }
  },
  {
    value: 'winter',
    payload: {
      title: 'Winter',
      subTitle: 'Oct-Dec'
    }
  }
])
const customValue2 = ref(customOptions2[0].value)
</script>
<template>
  <div>
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Segmented v-model:value="value" :options="options" @change="onChange" />
    <h2 class="mt30 mb10">禁用</h2>
    <Space vertical>
      <Segmented v-model:value="value" disabled :options="options" />
      <Segmented v-model:value="value2" :options="optionsDisabled" />
    </Space>
    <h2 class="mt30 mb10">动态加载数据</h2>
    <Space vertical>
      <Segmented v-model:value="dynamicValue" :options="dynamicOptions" />
      <Button type="primary" :loading="loading" :disabled="disabled" @click="loadMore">Load More</Button>
    </Space>
    <h2 class="mt30 mb10">block 分段控制器</h2>
    <Space :width="600">
      <Segmented v-model:value="value" block :options="options" />
    </Space>
    <h2 class="mt30 mb10">自定义渲染</h2>
    <Space vertical>
      <Segmented v-model:value="customValue" :options="customOptions1">
        <template #label="{ label, payload = {} }">
          <div style="padding: 4px">
            <template v-if="payload.icon">
              <Avatar :style="payload.style">
                <template #icon>
                  <svg
                    focusable="false"
                    class="u-icon"
                    data-icon="user"
                    width="1em"
                    height="1em"
                    fill="currentColor"
                    aria-hidden="true"
                    viewBox="64 64 896 896"
                  >
                    <path
                      d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"
                    ></path>
                  </svg>
                </template>
                {
  { payload.content }}
              </Avatar>
            </template>
            <template v-else>
              <Avatar :src="payload.src" :style="payload.style">
                {
  { payload.content }}
              </Avatar>
            </template>
            <div>{
  { label }}</div>
          </div>
        </template>
      </Segmented>
      <Segmented v-model:value="customValue2" :options="customOptions2">
        <template #label="{ payload }">
          <div style="padding: 4px 4px">
            <div>{
  { payload.title }}</div>
            <div>{
  { payload.subTitle }}</div>
          </div>
        </template>
      </Segmented>
    </Space>
    <h2 class="mt30 mb10">三种大小</h2>
    <Space vertical>
      <Segmented v-model:value="value" :options="options" size="large" />
      <Segmented v-model:value="value" :options="options" />
      <Segmented v-model:value="value" :options="options" size="small" />
    </Space>
  </div>
</template>
<style lang="less" scoped>
.u-icon {
  display: inline-block;
  fill: #fff;
}
</style>
AI 代码解读
目录
打赏
0
0
0
0
23
分享
相关文章
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
420 0
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
380 5
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
169 17
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
183 6
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
231 2
高效工作流:用Mermaid绘制你的专属流程图;如何在Vue3中导入mermaid绘制流程图
mermaid是一款非常优秀的基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。非常适合新手学习或者做一些弱交互且自定义要求不高的图表 除了流程图以外,mermaid还支持序列图、类图、状态图、实体关系图等图表可供探索。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
管理数据必备;侦听器watch用法详解,vue2与vue3中watch的变化与差异
一篇文章同时搞定Vue2和Vue3的侦听器,是不是很棒?不要忘了Vue3中多了一个可选项watchEffect噢。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
onMounted作为vue3中最常用的钩子函数之一,能够灵活、随心应手的使用是每个Vue开发者的必修课,同时根据其不同写法的特性,来选择最合适最有利于维护的写法。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
302 4

热门文章

最新文章

AI助理
登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问

你好,我是AI助理

可以解答问题、推荐解决方案等