---
title: "PillGroup"
description: "胶囊式单选/多选选项组，支持字符串与对象选项、数量约束、字段映射和插槽定制。"
seo_title: "PillGroup"
seo_description: "A pill-style single/multiple selection group supporting string and object items, min/max constraints, custom field mapping, per-item color and slot customization."
canonical_url: "https://nuxt.mhaibaraai.cn/docs/components/pill-group"
---
# PillGroup

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

## 简介

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

## 用法

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

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

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

### `items` 结构化选项

对象 `items` 可通过 `label`、`description` 与 `icon` 渲染复合内容，同时保留原始对象作为值：

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

<template>
  <MPillGroup :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` 多选模式

开启 `multiple` 后 `modelValue` 变为数组；配合 `valueKey` 让选中值只保存稳定字段：

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

<template>
  <MPillGroup :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` 允许再次点击当前项清空选择，适合非必选筛选条件（多选模式下忽略该属性）：

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

<template>
  <MPillGroup :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` 将选项纵向堆叠，适合描述较长或窄容器场景：

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

<template>
  <MPillGroup :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` 激活样式

`activeVariant` 与 `inactiveVariant` 分别控制选中与未选状态的视觉变体：

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

<template>
  <MPillGroup :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`：

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

<template>
  <MPillGroup :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` 或按钮组配色协调：

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

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

### `min` 与 `max` 数量约束

`min` 与 `max` 约束多选数量：触顶时未选项变灰，触底时已选项不可取消：

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

<template>
  <MPillGroup :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`：

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

<template>
  <MPillGroup :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 而非整个对象：

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

<template>
  <MPillGroup :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`，渲染时按项应用，便于表达状态差异：

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

<template>
  <MPillGroup :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` 冻结整组并保留当前值：

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

<template>
  <MPillGroup :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" :disabled="false" />
</template>
```

## 示例

### 插槽定制选项内容

`item-label`、`item-trailing` 等插槽可接管单项的局部渲染，选中标记随 `selected` slot prop 更新：

```vue [ComponentsPillGroupSlotsExample.vue]
<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:modelValue`、`change` 与 `select`，`select` 的 payload 含 `item`、`value`、`selected`、`index`：

```vue [ComponentsPillGroupEventsExample.vue]
<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

```ts
/**
 * Props for the MPillGroup component
 */
interface MPillGroupProps {
  items?: T[] | undefined;
  valueKey?: VK | undefined;
  /**
   * @default "\"label\""
   */
  labelKey?: GetItemKeys<T> | undefined;
  /**
   * @default "\"description\""
   */
  descriptionKey?: GetItemKeys<T> | undefined;
  /**
   * 对象模式下判定 selected 的比较依据。string 走 `getPath` 比较（multiple=true 时启用 Map O(1)）；
   * function 自定义比较；不传则按 [valueKey, 'value', labelKey] 启发式回退。
   */
  by?: string | ((a: T, b: T) => boolean) | undefined;
  /**
   * 排列方向
   * @default "\"horizontal\""
   */
  orientation?: "horizontal" | "vertical" | undefined;
  /**
   * 按钮尺寸
   */
  size?: "xs" | "sm" | "md" | "lg" | "xl" | undefined;
  /**
   * 按钮颜色
   */
  color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral" | undefined;
  /**
   * 选中态单项视觉变体
   * @default "\"solid\""
   */
  activeVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost" | undefined;
  /**
   * 未选中态单项视觉变体
   * @default "\"soft\""
   */
  inactiveVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost" | undefined;
  disabled?: boolean | undefined;
  /**
   * 是否可以多选
   */
  multiple?: M | undefined;
  /**
   * 单选：再次点击当前选中项清空（多选模式下被忽略）
   * @default "false"
   */
  deselectable?: boolean | undefined;
  /**
   * 多选：最多可选数量
   */
  max?: number | undefined;
  /**
   * 多选：最少需保留数量
   */
  min?: number | undefined;
  defaultValue?: GetModelValue<T, VK, M, undefined> | undefined;
  modelValue?: (GetModelValue<T, VK, M, undefined> & GetModelValue<T, VK, M>) | undefined;
  ui?: (Record<string, C> & { root?: SlotClass; list?: SlotClass; item?: SlotClass; leading?: SlotClass; leadingIcon?: SlotClass; trailing?: SlotClass; trailingIcon?: SlotClass; itemWrapper?: SlotClass; itemLabel?: SlotClass; itemDescription?: SlotClass; }) | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the MPillGroup component
 */
interface MPillGroupEmits {
  blur: (payload: [event: FocusEvent]) => void;
  focus: (payload: [event: FocusEvent]) => void;
  change: (payload: [value: GetModelValue<T, VK, M, undefined>]) => void;
  update:modelValue: (payload: [value: GetModelValue<T, VK, M, undefined>]) => void;
  select: (payload: [payload: PillSelectPayload<T>]) => void;
  update:modelValue: (payload: [value: GetModelValue<T, VK, M> | undefined]) => void;
}
```

### Slots

```ts
/**
 * Slots for the MPillGroup component
 */
interface MPillGroupSlots {
  leading(): any;
  default(): any;
  trailing(): any;
  item(): any;
  item-leading(): any;
  item-label(): any;
  item-description(): any;
  item-trailing(): any;
}
```

## Theme

<component-theme></component-theme>## Changelog

See commit history for [src/runtime/components/PillGroup.vue](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/components/PillGroup.vue).


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
