使用
该 SelectMenu
组件默认渲染一个 Select 组件,并且基于 ui.select
预设。如果不想覆盖默认插槽,可以使用大多数 Select
属性来配置显示,例如 color,variant,size,placeholder,icon,disabled 等。
您可以像 Select
组件一样使用 ui
属性来覆盖默认配置。该 uiMenu
属性可用于覆盖默认菜单配置。
与 Select
组件一样,您可以使用 options
属性来传递字符串或对象数组。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" />
</template>
多选
您可以使用 multiple
属性来选择多个值。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
</template>
对象
您可以将对象数组传递给 options
,并根据整个对象进行比较,或者使用 by
属性根据特定键进行比较。您可以通过 option-attribute
属性配置将用于显示标签的字段,默认值为 label
。此外,可以使用点符号(例如,user.name
)来访问嵌套的对象属性。
<script setup lang="ts">
import type { Avatar } from '#ui/types'
const people = [{
id: 'benjamincanac',
label: 'benjamincanac',
href: 'https://github.com/benjamincanac',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
}, {
id: 'Atinux',
label: 'Atinux',
href: 'https://github.com/Atinux',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
}, {
id: 'smarroufin',
label: 'smarroufin',
href: 'https://github.com/smarroufin',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
}, {
id: 'nobody',
label: 'Nobody',
icon: 'i-heroicons-user-circle'
}]
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people">
<template #leading>
<UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-5 h-5" />
<UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="2xs" />
</template>
</USelectMenu>
</template>
如果您只想选择单个对象属性而不是整个对象作为值,可以设置 value-attribute
属性。此属性默认为 null
。选项中 value-attribute
字段的值必须是唯一的。
<script setup lang="ts">
const people = [{
id: 1,
name: 'Wade Cooper'
}, {
id: 2,
name: 'Arlene Mccoy'
}, {
id: 3,
name: 'Devon Webb'
}, {
id: 4,
name: 'Tom Cook'
}]
const selected = ref(people[0].id)
</script>
<template>
<USelectMenu
v-model="selected"
:options="people"
placeholder="Select people"
value-attribute="id"
option-attribute="name"
/>
</template>
图标
使用 selected-icon
属性设置不同的图标,或者在 ui.selectMenu.default.selectedIcon
中全局更改它。默认值为 i-heroicons-check-20-solid
。
<template>
<USelectMenu
selected-icon="i-heroicons-hand-thumb-up-solid"
class="w-full lg:w-48"
placeholder="Select a person"
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
</template>
可搜索
使用 searchable
属性启用搜索。
使用 searchable-placeholder
属性设置不同的占位符,或通过 ui.selectMenu.default.searchablePlaceholder.label
配置全局设置。默认值为 Search...
。
这将使用 Headless UI Combobox 组件而不是 Listbox。
<template>
<USelectMenu
searchable
searchable-placeholder="Search a person..."
class="w-full lg:w-48"
placeholder="Select a person"
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
</template>
属性
使用带有属性名称数组的 search-attributes
属性在每个选项对象上进行搜索。可以使用 dot.notation
访问嵌套属性。当属性值为数组或对象时,这些值会被强制转换为字符串,以便可以在其中进行搜索。
<script setup lang="ts">
const options = [
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
]
const selected = ref(options[1])
</script>
<template>
<USelectMenu
v-model="selected"
:options="options"
placeholder="Select a person"
searchable
searchable-placeholder="Search by name or color"
option-attribute="name"
by="id"
:search-attributes="['name', 'colors']"
>
<template #option="{ option: person }">
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>
</template>
关闭时清除
默认情况下,搜索查询将在菜单关闭后保留。要关闭时清除它,请设置 clear-search-on-close
属性。
您也可以通过 ui.selectMenu.default.clearSearchOnClose
配置全局设置。默认值为 false
。
<template>
<USelectMenu
clear-search-on-close
class="w-full lg:w-48"
placeholder="Select a person"
searchable
searchable-placeholder="Search a person..."
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
</template>
控制查询
使用 v-model:query
来控制搜索查询。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref()
const query = ref('Wade')
</script>
<template>
<USelectMenu
v-model="selected"
v-model:query="query"
:options="people"
placeholder="Select a person"
searchable
/>
</template>
异步搜索
将一个函数传递给 searchable
属性,以自定义搜索行为并根据您的需求过滤选项。该函数将接收查询作为其第一个参数,并应返回一个数组。
使用 debounce
属性调整函数的延迟。
使用 searchableLazy
属性来控制数据请求的及时性。
<script setup lang="ts">
const loading = ref(false)
const selected = ref([])
async function search(q: string) {
loading.value = true
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false
return users
}
</script>
<template>
<USelectMenu
v-model="selected"
:loading="loading"
:searchable="search"
placeholder="Search for a user..."
option-attribute="name"
multiple
trailing
by="id"
/>
</template>
可创建
使用 creatable
属性启用在搜索未返回任何结果时创建新选项(仅适用于 searchable
)。
尝试搜索以下示例中不存在的内容。
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB(i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString(str) {
return intToRGB(hashCode(str))
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
</USelectMenu>
</template>
但是,如果您希望在搜索查询时创建选项(除了完全匹配之外),可以将 show-create-option-when
属性设置为 'always'
。
您也可以通过 ui.selectMenu.default.showCreateOptionWhen
配置全局设置。默认值为 empty
。
尝试搜索以下示例中存在的内容,但不是完全匹配。
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug' },
{ id: 2, name: 'documentation' },
{ id: 3, name: 'duplicate' },
{ id: 4, name: 'enhancement' },
{ id: 5, name: 'good first issue' },
{ id: 6, name: 'help wanted' },
{ id: 7, name: 'invalid' },
{ id: 8, name: 'question' },
{ id: 9, name: 'wontfix' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
show-create-option-when="always"
placeholder="Select labels"
/>
</template>
将一个函数传递给 show-create-option-when
属性来控制是否显示创建选项。此函数接受两个参数:查询(作为第一个参数)和当前结果数组(作为第二个参数)。它应返回 true 以显示创建选项。
以下示例显示了如何在查询至少为三个字符长且与任何当前结果(不区分大小写)不完全匹配时使创建选项可见。
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug' },
{ id: 2, name: 'documentation' },
{ id: 3, name: 'duplicate' },
{ id: 4, name: 'enhancement' },
{ id: 5, name: 'good first issue' },
{ id: 6, name: 'help wanted' },
{ id: 7, name: 'invalid' },
{ id: 8, name: 'question' },
{ id: 9, name: 'wontfix' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
const showCreateOption = (query, results) => {
const lowercaseQuery = String.prototype.toLowerCase.apply(query || '')
return lowercaseQuery.length >= 3 && !results.find((option) => {
return String.prototype.toLowerCase.apply(option['name'] || '') === lowercaseQuery
})
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
:show-create-option-when="showCreateOption"
placeholder="Select labels"
/>
</template>
Popper
使用 popper
属性自定义 popper 实例。
箭头
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" :popper="{ arrow: true }" />
</template>
放置
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" :popper="{ placement: 'left-end' }" />
</template>
偏移量
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
</template>
插槽
label
您可以覆盖 #label
插槽并自行处理显示。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
<span v-else>Select people</span>
</template>
</USelectMenu>
</template>
default
您还可以完全覆盖 #default
插槽。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray" class="flex-1 justify-between">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
option
使用 #option
插槽自定义选项内容。您将在插槽作用域中访问 option
、active
和 selected
属性。
<script setup lang="ts">
const people = [
{ name: 'Wade Cooper', online: true },
{ name: 'Arlene Mccoy', online: false },
{ name: 'Devon Webb', online: false },
{ name: 'Tom Cook', online: true },
{ name: 'Tanya Fox', online: false },
{ name: 'Hellen Schmidt', online: true },
{ name: 'Caroline Schultz', online: true },
{ name: 'Mason Heaney', online: false },
{ name: 'Claudie Smitham', online: true },
{ name: 'Emil Schaefer', online: false }
]
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-model="selected" :options="people" option-attribute="name">
<template #label>
<span :class="[selected.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ selected.name }}</span>
</template>
<template #option="{ option: person }">
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>
</template>
option-empty
使用 #option-empty
插槽自定义在 searchable
属性为 true
且没有选项时显示的内容。您将在插槽作用域中访问 query
属性。
您也可以通过 ui.selectMenu.default.optionEmpty.label
配置全局设置。令牌 {query}
将被 query
属性替换。默认值为 No results for "{query}".
。
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" searchable>
<template #option-empty="{ query }">
<q>{{ query }}</q> not found
</template>
</USelectMenu>
</template>
option-create
使用 #option-create
插槽自定义在 creatable
属性为 true
且没有选项时显示的内容。您将在插槽作用域中访问 query
属性。
empty
使用 #empty
插槽自定义在没有选项时显示的内容。
您也可以通过 ui.selectMenu.default.empty.label
配置全局设置。默认值为 No options.
。
<script setup lang="ts">
const people = []
const selected = ref()
</script>
<template>
<USelectMenu v-model="selected" :options="people">
<template #empty>
No people
</template>
</USelectMenu>
</template>
属性
null
null
"md"
"2xs"
"xs"
"sm"
"lg"
"xl"
config.default.color
null
{}
null
""
[]
config.default.variant
"outline"
"none"
null
config.default.loadingIcon
null
config.default.trailingIcon
undefined
{}
null
configMenu.default.selectedIcon
"label"
null
null
200
{}
null
false
configMenu.default.searchablePlaceholder.label
configMenu.default.showCreateOptionWhen
false
false
false
false
false
true
false
false
configMenu.default.clearSearchOnClose
false
配置
ui
属性覆盖选择配置,使用 uiMenu
属性覆盖菜单配置。{
container: 'z-20 group',
trigger: 'flex items-center w-full',
width: 'w-full',
height: 'max-h-60',
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
background: 'bg-white dark:bg-gray-800',
shadow: 'shadow-lg',
rounded: 'rounded-md',
padding: 'p-1',
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
option: {
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
rounded: 'rounded-md',
padding: 'px-1.5 py-1.5',
size: 'text-sm',
color: 'text-gray-900 dark:text-white',
container: 'flex items-center gap-1.5 min-w-0',
active: 'bg-gray-100 dark:bg-gray-900',
inactive: '',
selected: 'pe-7',
disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
icon: {
base: 'flex-shrink-0 h-5 w-5',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
selectedIcon: {
wrapper: 'absolute inset-y-0 end-0 flex items-center',
padding: 'pe-2',
base: 'h-5 w-5 text-gray-900 dark:text-white flex-shrink-0'
},
avatar: {
base: 'flex-shrink-0',
size: '2xs'
},
chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
},
create: 'block truncate'
},
transition: {
leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0'
},
popper: {
placement: 'bottom-end'
},
default: {
selectedIcon: 'i-heroicons-check-20-solid',
clearSearchOnClose: false,
showCreateOptionWhen: 'empty',
searchablePlaceholder: {
label: 'Search...'
},
empty: {
label: 'No options.'
},
optionEmpty: {
label: 'No results for "{query}".'
}
},
arrow: {
base: 'invisible before:visible before:block before:rotate-45 before:z-[-1] before:w-2 before:h-2',
ring: 'before:ring-1 before:ring-gray-200 dark:before:ring-gray-700',
rounded: 'before:rounded-sm',
background: 'before:bg-white dark:before:bg-gray-700',
shadow: 'before:shadow',
placement: "group-data-[popper-placement*='right']:-left-1 group-data-[popper-placement*='left']:-right-1 group-data-[popper-placement*='top']:-bottom-1 group-data-[popper-placement*='bottom']:-top-1"
},
select: 'inline-flex items-center text-left cursor-default',
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
required: 'absolute inset-0 w-px opacity-0 cursor-default',
label: 'block truncate'
}