PillGroup
简介
MPillGroup 是一个胶囊(pill)样式的选项组组件,支持单选与多选。接受字符串数组或对象数组作为选项,通过 labelKey、valueKey、descriptionKey 映射业务对象字段,支持数量约束、按项语义色、激活 / 未激活样式区分以及插槽定制。
用法
传入字符串数组即可生成单选胶囊,选中值直接写入 v-model:
<script setup lang="ts">
const value = ref("全部")
</script>
<template>
<MPillGroup v-model="value" :items='["全部","待办","进行中","已完成","已归档"]' />
</template>
items 结构化选项
对象 items 可通过 label、description 与 icon 渲染复合内容,同时保留原始对象作为值:
<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 多选模式
开启 multiple 后 modelValue 变为数组;配合 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 激活样式
activeVariant 与 inactiveVariant 分别控制选中与未选状态的视觉变体:
<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>
min 与 max 数量约束
min 与 max 约束多选数量:触顶时未选项变灰,触底时已选项不可取消:
<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-label、item-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:modelValue、change 与 select,select 的 payload 含 item、value、selected、index:
(点击选项查看事件)
<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"排列方向 |
items | T[] | |
valueKey | VK | |
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>> |
by | string | (a: T, b: T): boolean对象模式下判定 selected 的比较依据。string 走 | |
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"未选中态单项视觉变体 |
multiple | M是否可以多选 | |
max | number多选:最多可选数量 | |
min | number多选:最少需保留数量 | |
defaultValue | GetModelValue<T, VK, M, undefined> | |
modelValue | GetModelValue<T, VK, M, undefined> & GetModelValue<T, VK, M> | |
disabled | boolean | |
deselectable | false | boolean单选:再次点击当前选中项清空(多选模式下被忽略) |
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
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'
}
}
}
}
}
})