PillGroup

View source
胶囊式单选/多选选项组,支持字符串与对象选项、数量约束、字段映射和插槽定制。

简介

MPillGroup 是一个胶囊(pill)样式的选项组组件,支持单选与多选。接受字符串数组或对象数组作为选项,通过 labelKeyvalueKeydescriptionKey 映射业务对象字段,支持数量约束、按项语义色、激活 / 未激活样式区分以及插槽定制。

用法

传入字符串数组即可生成单选胶囊,选中值直接写入 v-model:

<script setup lang="ts">
const value = ref("全部")
</script>

<template>
  <MPillGroup v-model="value" :items='["全部","待办","进行中","已完成","已归档"]' />
</template>

items 结构化选项

对象 items 可通过 labeldescriptionicon 渲染复合内容,同时保留原始对象作为值:

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', description: '基础功能 · 1 用户', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', description: '团队协作 · 10 用户', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', description: '数据洞察 · 50 用户', icon: 'i-lucide-users' },
      {
        value: 'enterprise',
        label: '企业版',
        description: '自定义 SLA · 不限',
        icon: 'i-lucide-building'
      }
    ]"
  />
</template>

multiple 多选模式

开启 multiplemodelValue 变为数组;配合 valueKey 让选中值只保存稳定字段:

<script setup lang="ts">
const value = ref([])
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', icon: 'i-lucide-users' },
      { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
    ]"
    multiple
    value-key="value"
  />
</template>

deselectable 单选可取消

deselectable 允许再次点击当前项清空选择,适合非必选筛选条件(多选模式下忽略该属性):

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', icon: 'i-lucide-users' },
      { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
    ]"
    deselectable
  />
</template>

orientation 排列方向

orientation 设为 vertical 将选项纵向堆叠,适合描述较长或窄容器场景:

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', description: '基础功能 · 1 用户', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', description: '团队协作 · 10 用户', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', description: '数据洞察 · 50 用户', icon: 'i-lucide-users' },
      {
        value: 'enterprise',
        label: '企业版',
        description: '自定义 SLA · 不限',
        icon: 'i-lucide-building'
      }
    ]"
    orientation="vertical"
  />
</template>

activeVariant 激活样式

activeVariantinactiveVariant 分别控制选中与未选状态的视觉变体:

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', icon: 'i-lucide-users' },
      { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
    ]"
    active-variant="solid"
    inactive-variant="outline"
  />
</template>

size 尺寸

通过 size 切换胶囊尺寸;放入 UFormField 时会自动继承上下文中的 size

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
      { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
      { value: 'team', label: '团队版', icon: 'i-lucide-users' },
      { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
    ]"
    size="md"
  />
</template>

color 主色

通过 color 统一组件主色,便于与外部 UFieldGroup 或按钮组配色协调:

<script setup lang="ts">
const value = ref('全部')
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="['全部', '待办', '进行中', '已完成', '已归档']"
    color="primary"
  />
</template>

minmax 数量约束

minmax 约束多选数量:触顶时未选项变灰,触底时已选项不可取消:

<script setup lang="ts">
const value = ref(['frontend', 'design'])
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'frontend', label: '前端', icon: 'i-lucide-layout' },
      { value: 'backend', label: '后端', icon: 'i-lucide-server' },
      { value: 'devops', label: '运维', icon: 'i-lucide-cloud' },
      { value: 'design', label: '设计', icon: 'i-lucide-palette' },
      { value: 'data', label: '数据', icon: 'i-lucide-database' }
    ]"
    multiple
    value-key="value"
    :min="1"
    :max="3"
  />
</template>

labelKey 展示字段

labelKey 指定从业务对象读取的展示字段,未传时回退到 label

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { id: 'u1', name: 'Alice', handle: '@alice', email: 'alice@team.dev' },
      { id: 'u2', name: 'Bob', handle: '@bob', email: 'bob@team.dev' },
      { id: 'u3', name: 'Carol', handle: '@carol', email: 'carol@team.dev' }
    ]"
    label-key="name"
  />
</template>

valueKey 值字段

valueKey 指定 modelValue 写回的字段,便于只保存稳定 id 而非整个对象:

<script setup lang="ts">
const value = ref('u1')
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { id: 'u1', name: 'Alice', handle: '@alice', email: 'alice@team.dev' },
      { id: 'u2', name: 'Bob', handle: '@bob', email: 'bob@team.dev' },
      { id: 'u3', name: 'Carol', handle: '@carol', email: 'carol@team.dev' }
    ]"
    label-key="name"
    value-key="id"
  />
</template>

color 选项语义色

每个选项可携带独立 color,渲染时按项应用,便于表达状态差异:

<script setup lang="ts">
const value = ref('doing')
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'todo', label: '待办', icon: 'i-lucide-circle', color: 'neutral' },
      { value: 'doing', label: '进行中', icon: 'i-lucide-loader', color: 'warning' },
      { value: 'done', label: '已完成', icon: 'i-lucide-check-circle', color: 'success' },
      { value: 'block', label: '阻塞', icon: 'i-lucide-octagon-x', color: 'error' }
    ]"
    value-key="value"
  />
</template>

disabled 禁用

单项 disabled 阻止该项交互;组件级 disabled 冻结整组并保留当前值:

<script setup lang="ts">
const value = ref('pdf')
</script>

<template>
  <MPillGroup
    v-model="value"
    :items="[
      { value: 'pdf', label: 'PDF', icon: 'i-lucide-file-text' },
      {
        value: 'excel',
        label: 'Excel(敬请期待)',
        icon: 'i-lucide-file-spreadsheet',
        disabled: true
      },
      { value: 'csv', label: 'CSV', icon: 'i-lucide-file' },
      { value: 'json', label: 'JSON', icon: 'i-lucide-braces' }
    ]"
    value-key="value"
  />
</template>

示例

插槽定制选项内容

item-labelitem-trailing 等插槽可接管单项的局部渲染,选中标记随 selected slot prop 更新:

<script setup lang="ts">
import type { PillItem } from '@movk/nuxt'

const items: PillItem[] = [
  { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
  { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
  { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
]
const value = ref<PillItem[]>([items[1]!])

function getItemLabel(item: PillItem): string {
  if (typeof item === 'object' && item) return item.label ?? String(item.value)
  return String(item)
}
</script>

<template>
  <MPillGroup v-model="value" :items="items" multiple>
    <template #item-label="{ item }">
      <span class="font-semibold">{{ getItemLabel(item) }}</span>
    </template>
    <template #item-trailing="{ selected }">
      <UIcon v-if="selected" name="i-lucide-check" class="size-3.5" />
    </template>
  </MPillGroup>
</template>

事件回调

点击选项依次触发 update:modelValuechangeselectselect 的 payload 含 itemvalueselectedindex

(点击选项查看事件)

<script setup lang="ts">
import type { PillItem, PillSelectPayload } from '@movk/nuxt'

const items: PillItem[] = [
  { value: 'free', label: '免费版', icon: 'i-lucide-gift' },
  { value: 'pro', label: '专业版', icon: 'i-lucide-zap' },
  { value: 'enterprise', label: '企业版', icon: 'i-lucide-building' }
]
const value = ref<PillItem[]>([items[1]!])
const last = ref<string>('(点击选项查看事件)')

function onSelect(payload: PillSelectPayload<PillItem>) {
  const label =
    typeof payload.item === 'object' && payload.item
      ? (payload.item.label ?? String(payload.item.value))
      : String(payload.item)
  last.value = `select: ${label} → selected=${payload.selected}, index=${payload.index}`
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <MPillGroup v-model="value" :items="items" multiple @select="onSelect" />
    <p class="text-sm text-muted">
      {{ last }}
    </p>
  </div>
</template>

API

Props

Prop Default Type
orientation'horizontal'"horizontal" | "vertical"

排列方向

itemsT[]
valueKeyVK
labelKey'label'keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>
descriptionKey'description'keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>
bystring | (a: T, b: T): boolean

对象模式下判定 selected 的比较依据。string 走 getPath 比较(multiple=true 时启用 Map O(1)); function 自定义比较;不传则按 valueKey, 'value', labelKey 启发式回退。

size'md'"xs" | "sm" | "md" | "lg" | "xl"

按钮尺寸

color'primary'"primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral"

按钮颜色

activeVariant'solid'"link" | "solid" | "outline" | "soft" | "subtle" | "ghost"

选中态单项视觉变体

inactiveVariant'soft'"link" | "solid" | "outline" | "soft" | "subtle" | "ghost"

未选中态单项视觉变体

multipleM

是否可以多选

maxnumber

多选:最多可选数量

minnumber

多选:最少需保留数量

defaultValueGetModelValue<T, VK, M, undefined>
modelValueGetModelValue<T, VK, M, undefined> & GetModelValue<T, VK, M>
disabledboolean
deselectablefalseboolean

单选:再次点击当前选中项清空(多选模式下被忽略)

ui{ root?: ClassNameValue; list?: ClassNameValue; item?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; trailing?: ClassNameValue; trailingIcon?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; }

Emits

Event Type
blur[event: FocusEvent]
focus[event: FocusEvent]
change[value: GetModelValue<T, VK, M, undefined>]
update:modelValue[value: GetModelValue<T, VK, M, undefined>]
select[payload: PillSelectPayload<T>]

Slots

Slot Type
leading{ modelValue: GetModelValue<T, VK, M, undefined>; items: T[]; }
default{ modelValue: GetModelValue<T, VK, M, undefined>; items: T[]; }
trailing{ modelValue: GetModelValue<T, VK, M, undefined>; items: T[]; }
item{ item: T; index: number; selected: boolean; }
item-leading{ item: T; index: number; selected: boolean; }
item-label{ item: T; index: number; selected: boolean; }
item-description{ item: T; index: number; selected: boolean; }
item-trailing{ item: T; index: number; selected: boolean; }

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    pillGroup: {
      slots: {
        root: 'flex items-center gap-2',
        list: 'relative flex group rounded-lg gap-1',
        item: 'inline-flex items-center gap-1.5',
        leading: 'inline-flex items-center shrink-0',
        leadingIcon: 'shrink-0',
        trailing: 'inline-flex items-center shrink-0',
        trailingIcon: 'shrink-0',
        itemWrapper: 'flex flex-col items-start min-w-0 gap-0.5',
        itemLabel: 'truncate',
        itemDescription: 'truncate text-xs'
      },
      variants: {
        orientation: {
          horizontal: {
            root: 'flex-col',
            list: 'w-full'
          },
          vertical: {
            list: 'flex-col'
          }
        },
        fieldGroup: {
          horizontal: {
            root: 'rounded-md overflow-hidden not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-within:z-[1]',
            list: 'rounded-md'
          },
          vertical: {
            root: 'rounded-md overflow-hidden not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-within:z-[1]',
            list: 'rounded-md'
          }
        }
      }
    }
  }
})

Changelog

No recent changes
Copyright © 2025 - 2026 YiXuan - MIT License