Nuxt UI v3-alpha 已发布!

试用它
组件

输入菜单

显示具有实时建议的自动完成输入框。

使用

InputMenu 组件默认渲染一个 输入框 组件,并基于 ui.input 预设。您可以使用大多数 输入框 属性来配置显示,例如 颜色变体大小占位符图标禁用 等。

您可以像 输入框 组件一样使用 ui 属性来覆盖默认配置。该 uiMenu 属性可用于覆盖默认菜单配置。

将字符串或对象的数组传递给 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>
  <UInputMenu v-model="selected" :options="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>
  <UInputMenu 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>
  </UInputMenu>
</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>
  <UInputMenu
    v-model="selected"
    :options="options"
    placeholder="Select a person"
    by="id"
    option-attribute="name"
    :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>
  </UInputMenu>
</template>

如果您只想选择单个对象属性而不是整个对象作为值,可以设置 value-attribute 属性。此属性默认为 null

<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>
  <UInputMenu
    v-model="selected"
    :options="people"
    value-attribute="id"
    option-attribute="name"
  />
</template>

图标

InputMenu 在右侧有一个按钮用于切换菜单。使用 trailing-icon 属性设置不同的图标或在 ui.inputMenu.default.trailingIcon 中全局更改它。默认为 i-heroicons-chevron-down-20-solid

<template>
  <UInputMenu
    trailing-icon="i-heroicons-chevron-up-down-20-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>

使用 selected-icon 属性设置不同的图标或在 ui.inputMenu.default.selectedIcon 中全局更改它。默认为 i-heroicons-check-20-solid

<template>
  <UInputMenu
    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>
了解如何从 输入框 组件自定义图标。

可搜索

属性

使用包含属性名称数组的 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>
  <UInputMenu
    v-model="selected"
    :options="options"
    placeholder="Select a person"
    by="id"
    option-attribute="name"
    :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>
  </UInputMenu>
</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>
  <UInputMenu
    v-model="selected"
    v-model:query="query"
    :options="people"
    placeholder="Select a person"
  />
</template>

将函数传递给 search 属性以自定义搜索行为并根据您的需求过滤选项。该函数将接收查询作为其第一个参数,并应返回一个数组。

使用 debounce 属性调整函数的延迟。

使用 searchLazy 属性控制数据请求的即时性。

<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>
  <UInputMenu
    v-model="selected"
    :search="search"
    :loading="loading"
    placeholder="Search for a user..."
    option-attribute="name"
    trailing
    by="id"
  />
</template>

弹出框

使用 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>
  <UInputMenu 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>
  <UInputMenu v-model="selected" :options="people" :popper="{ placement: 'right-start' }" />
</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>
  <UInputMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
</template>

插槽

option

使用 #option 插槽自定义选项内容。您将可以在插槽作用域中访问 optionactiveselected 属性。

<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>
  <UInputMenu v-model="selected" :options="people" option-attribute="name">
    <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>
  </UInputMenu>
</template>

option-empty

使用 #option-empty 插槽自定义当 searchable 属性为 true 且没有选项时显示的内容。您将可以在插槽作用域中访问 query 属性。

您也可以通过 ui.inputMenu.default.optionEmpty.label 配置全局设置。令牌 {query} 将被 query 属性替换。默认为 未找到 "{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>
  <UInputMenu v-model="selected" :options="people" searchable>
    <template #option-empty="{ query }">
      <q>{{ query }}</q> not found
    </template>
  </UInputMenu>
</template>

empty

使用 #empty 插槽自定义没有选项时显示的内容。默认为 没有选项。

您也可以通过 ui.inputMenu.default.empty.label 配置全局设置。默认为 没有选项。

<script setup lang="ts">
const people = []

const selected = ref()
</script>

<template>
  <UInputMenu v-model="selected" :options="people">
    <template #empty>
      No people
    </template>
  </UInputMenu>
</template>

属性

name
字符串
null
size
InputSize
null
"md""2xs""xs""sm""lg""xl"
search
(query: string) => any[] | Promise<any[]>
undefined
color
字符串
config.default.color
icon
字符串
null
ui
{ wrapper?: string; base?: string; form?: string; rounded?: string; placeholder?: string; file?: DeepPartial<{ base: string; }, any>; size?: DeepPartial<{ '2xs': string; xs: string; sm: string; md: string; lg: string; xl: string; }, any>; ... 7 more ...; default?: DeepPartial<...>; } & { ...; } & { ...; }
{}
id
字符串
null
modelValue
字符串 | 数字 | 对象 | 任何数组
""
inputClass
字符串
null
options
字符串数组 | { [key: string]: any; disabled?: boolean; }[]
[]
variant
InputVariant
config.default.variant
"outline""none"
placeholder
字符串
null
loadingIcon
字符串
config.default.loadingIcon
leadingIcon
字符串
null
trailingIcon
字符串
configMenu.default.trailingIcon
by
字符串
undefined
popper
PopperOptions
{}
query
字符串
null
selectedIcon
字符串
configMenu.default.selectedIcon
optionAttribute
字符串
"label"
valueAttribute
字符串
null
searchAttributes
未知数组
null
debounce
数字
200
uiMenu
{ container?: string; trigger?: string; width?: string; height?: string; base?: string; background?: string; shadow?: string; rounded?: string; padding?: string; ring?: string; empty?: string; option?: DeepPartial<{ base: string; ... 13 more ...; chip: { ...; }; }, any>; transition?: DeepPartial<...>; popper?: DeepP...
{}
required
布尔值
false
disabled
布尔值
false
可空
布尔值
false
leading
布尔值
false
trailing
布尔值
false
loading
布尔值
false
padded
布尔值
true
searchLazy
布尔值
false

配置

使用 ui 属性来覆盖输入配置,使用 uiMenu 属性来覆盖菜单配置。
{
  wrapper: 'relative',
  base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
  form: 'form-input',
  rounded: 'rounded-md',
  placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
  file: {
    base: 'file:mr-1.5 file:font-medium file:text-gray-500 dark:file:text-gray-400 file:bg-transparent file:border-0 file:p-0 file:outline-none'
  },
  size: {
    '2xs': 'text-xs',
    xs: 'text-xs',
    sm: 'text-sm',
    md: 'text-sm',
    lg: 'text-sm',
    xl: 'text-base'
  },
  gap: {
    '2xs': 'gap-x-1',
    xs: 'gap-x-1.5',
    sm: 'gap-x-1.5',
    md: 'gap-x-2',
    lg: 'gap-x-2.5',
    xl: 'gap-x-2.5'
  },
  padding: {
    '2xs': 'px-2 py-1',
    xs: 'px-2.5 py-1.5',
    sm: 'px-2.5 py-1.5',
    md: 'px-3 py-2',
    lg: 'px-3.5 py-2.5',
    xl: 'px-3.5 py-2.5'
  },
  leading: {
    padding: {
      '2xs': 'ps-7',
      xs: 'ps-8',
      sm: 'ps-9',
      md: 'ps-10',
      lg: 'ps-11',
      xl: 'ps-12'
    }
  },
  trailing: {
    padding: {
      '2xs': 'pe-7',
      xs: 'pe-8',
      sm: 'pe-9',
      md: 'pe-10',
      lg: 'pe-11',
      xl: 'pe-12'
    }
  },
  color: {
    white: {
      outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
    },
    gray: {
      outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
    }
  },
  variant: {
    outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
    none: 'bg-transparent focus:ring-0 focus:shadow-none'
  },
  icon: {
    base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
    color: 'text-{color}-500 dark:text-{color}-400',
    loading: 'animate-spin',
    size: {
      '2xs': 'h-4 w-4',
      xs: 'h-4 w-4',
      sm: 'h-5 w-5',
      md: 'h-5 w-5',
      lg: 'h-5 w-5',
      xl: 'h-6 w-6'
    },
    leading: {
      wrapper: 'absolute inset-y-0 start-0 flex items-center',
      pointer: 'pointer-events-none',
      padding: {
        '2xs': 'px-2',
        xs: 'px-2.5',
        sm: 'px-2.5',
        md: 'px-3',
        lg: 'px-3.5',
        xl: 'px-3.5'
      }
    },
    trailing: {
      wrapper: 'absolute inset-y-0 end-0 flex items-center',
      pointer: 'pointer-events-none',
      padding: {
        '2xs': 'px-2',
        xs: 'px-2.5',
        sm: 'px-2.5',
        md: 'px-3',
        lg: 'px-3.5',
        xl: 'px-3.5'
      }
    }
  },
  default: {
    size: 'sm',
    color: 'white',
    variant: 'outline',
    loadingIcon: 'i-heroicons-arrow-path-20-solid'
  }
}
{
  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'
    }
  },
  transition: {
    leaveActiveClass: 'transition ease-in duration-100',
    leaveFromClass: 'opacity-100',
    leaveToClass: 'opacity-0'
  },
  popper: {
    placement: 'bottom-end'
  },
  default: {
    selectedIcon: 'i-heroicons-check-20-solid',
    trailingIcon: 'i-heroicons-chevron-down-20-solid',
    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"
  }
}