资讯动态

Vue3选择器(Select)

发布时间:2026/8/26 13:39:25 来源:尧图企业网站定制
Vue2选择器Select可自定义设置以下属性选项数据options类型Option[]默认值 []其中Option类型{label?: string, value?: string | number, disabled?: boolean, [propName: string]: any}选择器字典项的文本字段名label类型string默认 label选择器字典项的值字段名value类型string默认 value默认占位文本placeholder类型string默认 请选择是否禁用下拉disabled类型boolean默认 false选择器宽度width类型string | number默认 auto选择器高度height类型number单位 px默认 undefined选择器大小size类型small | middle | large默认 middle是否支持清除allowClear类型boolean默认 false是否支持搜索search类型boolean默认 false过滤条件函数filter类型Function | true默认 true仅当支持搜索时生效根据输入项进行筛选默认为 true 时筛选每个选项的文本字段 label 是否包含输入项包含返回 true反之返回 false当其为函数 Function 时接受 inputValue option 两个参数当 option 符合筛选条件时应返回 true反之则返回 false下拉面板弹出位置placement类型bottom | top默认 bottom下拉面板被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置flip类型boolean默认 true下拉面板挂载的容器节点to类型string | HTMLElement | false可选元素标签名 (例如 body) 或者元素本身false 会待在原地下拉面板最多能展示的项数超过后滚动显示maxDisplay类型number默认 6下拉面板滚动条 scrollbar 组件属性配置scrollbarProps类型object默认 {}当前选中的 option 条目值modelValue类型number | string默认 undefined效果如下图展开图支持搜索在线预览①创建选择器组件Select.vue其中引入使用使用了以下组件与工具函数Vue3空状态EmptyVue3滚动条ScrollbarDOM监听 useMutationObserver获取依赖注入 useInject​ script setup langts import { ref, computed, watchEffect, watch, nextTick, onMounted, onBeforeUnmount } from vue import type { CSSProperties } from vue import Empty from components/empty import Scrollbar from components/scrollbar import { useEventListener, useMutationObserver, useInject, useOptionsSupported } from components/utils export interface Option { label?: string // 选项名 value?: string | number // 选项值 disabled?: boolean // 是否禁用选项默认 false [propName: string]: any // 添加一个字符串索引签名用于包含带有任意数量的其他属性 } export interface Props { options?: Option[] // 选项数据 label?: string // 字典项的文本字段名 value?: string // 字典项的值字段名 placeholder?: string // 默认占位文本 disabled?: boolean // 是否禁用 width?: string | number // 选择器宽度单位 px height?: number // 选择器高度单位 px size?: small | middle | large // 选择器大小 allowClear?: boolean // 是否支持清除 search?: boolean // 是否支持搜索 placement?: bottom | top // 下拉面板弹出位置 flip?: boolean // 下拉面板被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置 to?: string | HTMLElement | false // 下拉面板挂载的容器节点可选元素标签名 (例如 body) 或者元素本身false 会待在原地 /* 根据输入项进行筛选默认为 true 时筛选每个选项的文本字段 label 是否包含输入项包含返回 true反之返回 false 当其为函数 Function 时接受 inputValue option 两个参数当 option 符合筛选条件时应返回 true反之则返回 false */ filter?: Function | true // 过滤条件函数仅当支持搜索时生效 maxDisplay?: number // 下拉面板最多能展示的项数超过后滚动显示 scrollbarProps?: object // 下拉面板滚动条 scrollbar 组件属性配置 modelValue?: number | string // (v-model) 当前选中的 option 条目值 } const props withDefaults(definePropsProps(), { options: () [], label: label, value: value, placeholder: 请选择, disabled: false, width: auto, height: undefined, size: middle, allowClear: false, search: false, placement: bottom, flip: true, to: body, filter: true, maxDisplay: 6, scrollbarProps: () ({}), modelValue: undefined }) const initialDisplay refboolean(false) // 性能优化使用 v-if 避免初始时不必要的渲染展示之后使用 v-show 来控制显示隐藏 const filterOptions refOption[]([]) // 过滤后的选项数组 const selectedName ref() // 当前选中选项的 label const inputRef refHTMLElement | null(null) // input 元素引用 const inputValue ref() // 支持搜索时用户输入内容 const disabledBlur refboolean(false) // 是否禁用 input 标签的 blur 事件 const hideSelectName refboolean(false) // 用户输入时隐藏 selectName 的展示 const hoverValue ref() // 鼠标悬浮项的 value 值 const showOptions refboolean(false) // 显示隐藏 options 面板 const showArrow refboolean(true) // 剪头图标显隐 const showClear refboolean(false) // 清除图标显隐 const showCaret refboolean(false) // 支持搜索时输入光标的显隐 const showSearch refboolean(false) // 搜索图标显隐 const selectFocused refboolean(false) /// select 是否聚焦 const { colorPalettes, shadowColor } useInject(Select) // 主题色注入 const scrollTarget refHTMLElement | null(null) // 最近的可滚动父元素 const scrollTop refnumber(0) // scrollTarget 的滚动位置 const panelOffset refnumber(0) // 下拉面板相对于 selectContent 的垂直偏移距离 const panelPlace refbottom | top(bottom) // 下拉面板位置 const selectContentRef refHTMLElement | null(null) // selectContent 模板引用 const selectContentRect refDOMRect() // selectContent 元素的大小及其相对于视口的位置 const positionedContainer refHTMLElement | null(null) // 下拉面板相对定位的容器元素 const positionedContainerRect refDOMRect() // positionedContainer 元素的大小及其相对于视口的位置 const selectPanelRef refHTMLElement | null(null) // 下拉面板 selectPanel 模板引用 const selectPanelHeight refnumber() // 下拉面板 selectPanel 的高度 const viewportWidth refnumber(document.documentElement.clientWidth) // 视口宽度(不包括滚动条) const viewportHeight refnumber(document.documentElement.clientHeight) // 视口高度(不包括滚动条) const { isSupported: passiveSupported } useOptionsSupported(passive) const emits defineEmits([update:modelValue, change, openChange]) const selectWidth computed(() { if (typeof props.width number) { return ${props.width}px } return props.width }) const selectHeight computed(() { const heightMap { small: 24, middle: 32, large: 40 } if (props.height ! undefined) { return ${props.height}px } return ${heightMap[props.size]}px }) // 是否存在滚动 const isScrollable computed(() { return props.options.length props.maxDisplay }) const optionsStyle computed(() { const style: CSSProperties { maxHeight: ${props.maxDisplay * 32 8}px } return style }) const panelPlacement computed(() { const contentTop (selectContentRect.value as DOMRect)?.top ?? 0 const containerTop (positionedContainerRect.value as DOMRect)?.top ?? 0 const offsetTop contentTop - containerTop const contentBottom (selectContentRect.value as DOMRect)?.bottom ?? 0 const containerBottom (positionedContainerRect.value as DOMRect)?.bottom ?? 0 const offsetBottom containerBottom - contentBottom const contentLeft (selectContentRect.value as DOMRect)?.left ?? 0 const containerLeft (positionedContainerRect.value as DOMRect)?.left ?? 0 const offsetLeft contentLeft - containerLeft const panelWidth (selectContentRect.value as DOMRect)?.width ?? 0 switch (panelPlace.value) { case bottom: return { transformOrigin: 0 0, top: ${offsetTop panelOffset.value}px, left: ${offsetLeft}px, minWidth: ${panelWidth}px, width: ${panelWidth}px } case top: return { transformOrigin: 100% 100%, bottom: ${offsetBottom panelOffset.value}px, left: ${offsetLeft}px, minWidth: ${panelWidth}px, width: ${panelWidth}px } default: return { transformOrigin: 0 0, top: ${offsetTop panelOffset.value}px, left: ${offsetLeft}px, minWidth: ${panelWidth}px, width: ${panelWidth}px } } }) watch([() props.placement, () props.flip], () { updatePosition() }) watch(showOptions, (to) { if (to !initialDisplay.value) { initialDisplay.value true } }) watch(showOptions, (to) { emits(openChange, to) if (props.search !to) { inputValue.value undefined hideSelectName.value false } }) watchEffect(() { if (props.search) { if (inputValue.value) { filterOptions.value props.options.filter((option) { if (typeof props.filter function) { return props.filter(inputValue.value, option) } else { return option[props.label].includes(inputValue.value) } }) } else { if (showOptions.value) { filterOptions.value [...props.options] } else { setTimeout(() { filterOptions.value [...props.options] }, 200) } } if (filterOptions.value.length inputValue.value) { hoverValue.value filterOptions.value[0][props.value] } else { hoverValue.value null } } else { filterOptions.value props.options } }) watchEffect(() { initSelector() }) onMounted(() { observeScroll() }) onBeforeUnmount(() { cleanup() }) // 监听 vitepress 文档页面滚动 const mutationObserver useMutationObserver( scrollTarget, () { if (scrollTop.value ! scrollTarget.value?.scrollTop) { scrollTop.value scrollTarget.value?.scrollTop ?? 0 updatePosition() } }, { subtree: true, attributes: true } ) useEventListener(window, resize, getViewportSize) function getPositionedContainer(): void { let parentElement selectPanelRef.value?.parentElement while (parentElement) { if (parentElement document.documentElement) { positionedContainer.value document.documentElement return } const { position } getComputedStyle(parentElement) if (position ! static) { positionedContainer.value parentElement return } parentElement parentElement.parentElement } } function getViewportSize() { viewportWidth.value document.documentElement.clientWidth viewportHeight.value document.documentElement.clientHeight observeScroll() // 窗口尺寸变化时重新查询并监听最近可滚动父元素 updatePosition() } // 查询并监听最近可滚动父元素 function observeScroll() { cleanup() scrollTarget.value getScrollParent(selectContentRef.value) scrollTarget.value scrollTarget.value.addEventListener( scroll, updatePosition, passiveSupported.value ? { passive: true } : undefined ) if (scrollTarget.value document.documentElement) { mutationObserver.start() } else { mutationObserver.stop() } } /** * 清理滚动监听事件并重置滚动目标。 * * 清理函数移除滚动事件监听并重置滚动目标 */ function cleanup() { scrollTarget.value scrollTarget.value.removeEventListener(scroll, updatePosition) scrollTarget.value null } // 获取父元素 function getParentElement(el: HTMLElement): HTMLElement | null { // Document if (el document.documentElement) return null return el.parentElement } // 查找最近的可滚动父元素 function getScrollParent(el: HTMLElement | null): HTMLElement | null { if (el null) return null const parentElement getParentElement(el) if (parentElement null) return null // Document if (parentElement document.documentElement) return document.documentElement const isScrollable (el: HTMLElement): boolean { const { overflow, overflowX, overflowY } getComputedStyle(el) return /(auto|scroll|overlay)/.test(overflow overflowY overflowX) } // Element if (isScrollable(parentElement)) return parentElement return getScrollParent(parentElement) } // 更新下拉面板位置 function updatePosition() { showOptions.value getPosition() } // 计算下拉面板位置 async function getPosition() { await nextTick() getPositionedContainer() positionedContainerRect.value positionedContainer.value?.getBoundingClientRect() as DOMRect selectContentRect.value selectContentRef.value?.getBoundingClientRect() as DOMRect selectPanelHeight.value selectPanelRef.value?.offsetHeight panelOffset.value selectContentRect.value.height 4 if (props.flip) { panelPlace.value getPlacement() } } // 获取可滚动父元素或视口的矩形信息 function getShelterRect() { if (scrollTarget.value) { const scrollTargetRect scrollTarget.value.getBoundingClientRect() return { top: scrollTargetRect.top 0 ? 0 : scrollTargetRect.top, bottom: scrollTargetRect.bottom viewportHeight.value ? viewportHeight.value : scrollTargetRect.bottom } } return { top: 0, bottom: viewportHeight.value } } // 下拉面板被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置 function getPlacement(): bottom | top { const { top, bottom } selectContentRect.value as DOMRect // 内容元素各边缘相对于浏览器视口的位置(不包括滚动条) const { top: targetTop, bottom: targetBottom } getShelterRect() // 滚动元素或视口各边缘相对于浏览器视口的位置(不包括滚动条) const topDistance top - targetTop // 内容元素上边缘距离滚动元素上边缘的距离 const bottomDistance targetBottom - bottom // 内容元素下边缘距离动元素下边缘的距离 return findPlace(props.placement, []) // 查询满足条件的 place如果没有则返回默认值 function findPlace(place: string, disabledPlaces: string[]): bottom | top { if (place bottom) { if (!disabledPlaces.includes(bottom)) { if (bottomDistance (selectPanelHeight.value as number) 4) { return findPlace(top, [...disabledPlaces, bottom]) } else { return bottom } } else { if (!disabledPlaces.includes(top)) { return findPlace(top, disabledPlaces) } } } else if (place top) { if (!disabledPlaces.includes(top)) { if (topDistance (selectPanelHeight.value as number) 4) { return findPlace(bottom, [...disabledPlaces, top]) } else { return top } } else { if (!disabledPlaces.includes(bottom)) { return findPlace(bottom, disabledPlaces) } } } return props.placement } } function initSelector(): void { if (props.modelValue) { const target props.options.find((option) option[props.value] props.modelValue) if (target) { selectedName.value target[props.label] hoverValue.value target[props.value] } else { selectedName.value props.modelValue hoverValue.value null } } else { selectedName.value null hoverValue.value null } } function onFocus(): void { selectFocused.value true } function onBlur(): void { selectFocused.value false if (showOptions.value) { showOptions.value false } if (props.search) { showSearch.value false showArrow.value true hideSelectName.value false } } function onEnter(): void { disabledBlur.value true if (props.allowClear) { if (selectedName.value || (props.search inputValue.value)) { showArrow.value false showClear.value true if (props.search) { showSearch.value false } } } } function onLeave(): void { disabledBlur.value false if (props.allowClear showClear.value) { showClear.value false if (!props.search) { showArrow.value true } } if (props.search) { if (showOptions.value) { showSearch.value true showArrow.value false } else { showSearch.value false showArrow.value true } } } function onHover(value: string | number, disabled: boolean | undefined): void { disabledBlur.value Boolean(disabled) hoverValue.value value } async function toggleSelect(): Promisevoid { selectFocus() if (!props.search inputRef.value) { inputRef.value.style.opacity 0 } showOptions.value !showOptions.value if (showOptions.value) { getPosition() } if (!hoverValue.value selectedName.value) { const target props.options.find((option) option[props.label] selectedName.value) hoverValue.value target ? target[props.value] : null } if (props.search) { if (!showClear.value) { showArrow.value !showOptions.value showSearch.value showOptions.value } } } function onSearchInput(e: Event): void { hideSelectName.value Boolean((e.target as HTMLInputElement)?.value) } function onClear(): void { if (selectFocused.value) { selectFocus() showCaret.value true } showClear.value false selectedName.value null hoverValue.value null showOptions.value false showSearch.value false showArrow.value true emits(update:modelValue) emits(change) } function selectFocus(): void { inputRef.value?.focus() // 通过 input 标签聚焦来模拟 select 整体聚焦效果 } // 选中下拉项后的回调 function onChange(value: string | number, label: string, index: number): void { if (props.modelValue ! value) { selectedName.value label hoverValue.value value emits(update:modelValue, value) emits(change, value, label, index) } showCaret.value false selectFocus() } /script template div classselect-wrap :class{ select-focused: selectFocused, search-select: search, select-small: size small, select-large: size large, select-disabled: disabled } :style --select-width: ${selectWidth}; --select-height: ${selectHeight}; --select-primary-color-hover: ${colorPalettes[4]}; --select-primary-color-focus: ${colorPalettes[4]}; --select-primary-shadow-color: ${shadowColor}; clickdisabled ? () false : toggleSelect() div refselectContentRef classselect-content-container mouseenteronEnter mouseleaveonLeave span classselect-search input refinputRef classsearch-input :class{ caret-show: showOptions || showCaret } typetext autocompleteoff :readonly!search :disableddisabled inputonSearchInput v-modelinputValue blur!disabledBlur !disabled ? onBlur() : () false focus!disabled ? onFocus() : () false / /span span classselect-item :class{ select-placeholder: !selectedName || showOptions, select-item-hidden: hideSelectName } :titleselectedName {{ selectedName || placeholder }} /span svg classarrow-svg :class{ arrow-rotate: showOptions, show-svg: showArrow } focusablefalse >script setup langts import Select from ./Select.vue import { ref, watchEffect } from vue import type { SelectProps, SelectOption } from vue-amazing-ui const options refSelectOption[]([ { label: 北京市, value: 1 }, { label: 上海市, value: 2 }, { label: 纽约市, value: 3 }, { label: 旧金山, value: 4 }, { label: 布宜诺斯艾利斯, value: 5 }, { label: 伊斯坦布尔, value: 6 }, { label: 拜占庭, value: 7 }, { label: 君士坦丁堡, value: 8 } ]) const optionsDisabled refSelectOption[]([ { label: 北京市, value: 1 }, { label: 上海市, value: 2, disabled: true }, { label: 纽约市, value: 3 }, { label: 旧金山, value: 4 }, { label: 布宜诺斯艾利斯, value: 5 }, { label: 伊斯坦布尔, value: 6 }, { label: 拜占庭, value: 7 }, { label: 君士坦丁堡, value: 8 } ]) const optionsCustom refSelectOption[]([ { name: 北京市, id: 1 }, { name: 上海市, id: 2 }, { name: 纽约市, id: 3 }, { name: 旧金山, id: 4 }, { name: 布宜诺斯艾利斯, id: 5 }, { name: 伊斯坦布尔, id: 6 }, { name: 拜占庭, id: 7 }, { name: 君士坦丁堡, id: 8 } ]) const sizeOptions [ { label: small, value: small }, { label: middle, value: middle }, { label: large, value: large } ] const placementOptions [ { label: bottom, value: bottom }, { label: top, value: top } ] const size ref(large) const selectedValue refSelectProps[modelValue](5) const placement ref(bottom) watchEffect(() { console.log(selectedValue, selectedValue.value) }) function onChange(value: string | number, label: string, index: number) { console.log(value, value) console.log(label, label) console.log(index, index) } function onOpenChange(open: boolean) { console.log(openChange, open) } // 自定义过滤函数当选项的 value 值大于 输入项时返回 true function filter(inputValue: string, option: any) { return option.value inputValue } /script template div h1{{ $route.name }} {{ $route.meta.title }}/h1 h2 classmt30 mb10基本使用/h2 Select :optionsoptions v-modelselectedValue changeonChange openChangeonOpenChange / h2 classmt30 mb10禁用/h2 Select :optionsoptions disabled v-modelselectedValue / h2 classmt30 mb10禁用选项/h2 Select :optionsoptionsDisabled v-modelselectedValue / h2 classmt30 mb10自定义节点字段名/h2 Select :optionsoptionsCustom labelname valueid v-modelselectedValue / h2 classmt30 mb10自定义样式/h2 Select :width150 :height36 search :optionsoptions v-modelselectedValue / h2 classmt30 mb10三种尺寸/h2 Space vertical Radio :optionssizeOptions v-model:valuesize button button-stylesolid / Select :optionsoptions v-modelselectedValue :sizesize / Select :optionsoptions search allow-clear v-modelselectedValue :sizesize / /Space h2 classmt30 mb10支持清除/h2 Select :optionsoptions allow-clear v-modelselectedValue / h2 classmt30 mb10支持搜索/h2 Select :optionsoptions search allow-clear v-modelselectedValue / h2 classmt30 mb10搜索过滤函数/h2 Select :optionsoptions search :filterfilter v-modelselectedValue / h2 classmt30 mb10下拉面板弹出位置/h2 Space vertical Radio :optionsplacementOptions v-model:valueplacement button button-stylesolid / Select :optionsoptions v-modelselectedValue :placementplacement / Select :optionsoptions search allow-clear v-modelselectedValue :placementplacement / /Space h2 classmt30 mb10下拉面板数/h2 Select :optionsoptions :max-display8 v-modelselectedValue / h2 classmt30 mb10下拉面板滚动条/h2 Select :optionsoptions v-modelselectedValue :scrollbar-props{ size: 8, delay: 2000 } / /div /template

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价