5454 @after-leave =" $emit('after-close')" >
5555 <div v-show =" isOpen"
5656 ref =" dropdown"
57- class =" fixed z-[99999] max-h-60 overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-blue-200 focus:outline-none"
57+ class =" fixed z-[99999] bg-white text-base shadow-lg ring-1 ring-blue-200 focus:outline-none rounded-md overflow-hidden "
5858 :class =" dropdownClasses"
5959 :style =" dropdownStyle"
6060 role =" listbox"
6161 :aria-labelledby =" buttonId" >
62+
6263 <!-- 搜索框 (可选) -->
63- <div v-if =" searchable" class =" sticky top-0 bg-white p-2 border-b border-gray-100" >
64+ <div v-if =" searchable" class =" p-2 border-b border-gray-100 bg-white " >
6465 <input v-model =" searchQuery"
6566 type =" text"
6667 class =" w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
6768 :placeholder =" searchPlaceholder"
6869 @click.stop
70+ @keydown.stop
6971 ref =" searchInput" />
7072 </div >
7173
72- <!-- 选项列表 -->
73- <template v-if =" filteredOptions .length > 0 " >
74- <div v-for =" (option, index) in filteredOptions"
75- :key =" getOptionValue(option)"
76- @click =" selectOption(option)"
77- @keydown.enter.prevent =" selectOption(option)"
78- @keydown.space.prevent =" selectOption(option)"
79- :class =" [
80- 'relative flex space-x-3 items-center cursor-pointer select-none py-1 my-1 pl-3 pr-9 transition-colors duration-150',
81- isSelected(option)
82- ? 'bg-blue-400 text-white'
83- : 'text-gray-900 hover:bg-blue-50',
84- highlightedIndex === index ? 'bg-blue-100' : ''
85- ]"
86- :aria-selected =" isSelected(option)"
87- role =" option"
88- tabindex =" -1" >
89- <!-- 如果是SVG字符串 -->
90- <div v-if =" getSvgIcon(option)" v-html =" getSvgIcon(option)" class =" w-6 h-6" />
91-
92- <!-- 如果是SVG URL -->
93- <img v-else-if =" getSvgUrl(option)" :src =" getSvgUrl(option)" class =" w-6 h-6" alt =" icon" />
94-
95- <span :class =" ['block truncate', isSelected(option) ? 'font-medium' : 'font-normal']" >
96- {{ getOptionLabel(option) }}
97- </span >
98-
99- <!-- 选中图标 -->
100- <span v-if =" isSelected(option)"
101- class =" absolute inset-y-0 right-0 flex items-center pr-2" >
102- <CheckIcon class =" h-5 w-5" aria-hidden =" true" />
103- </span >
74+ <!-- 选项列表容器 -->
75+ <div class =" max-h-60 overflow-auto" @scroll.stop >
76+ <!-- 选项列表 -->
77+ <template v-if =" filteredOptions .length > 0 " >
78+ <div v-for =" (option, index) in filteredOptions"
79+ :key =" getOptionValue(option)"
80+ @click =" selectOption(option)"
81+ @keydown.enter.prevent =" selectOption(option)"
82+ @keydown.space.prevent =" selectOption(option)"
83+ :class =" [
84+ 'relative flex space-x-3 items-center cursor-pointer select-none py-2 px-3 transition-colors duration-150',
85+ isSelected(option)
86+ ? 'bg-blue-400 text-white'
87+ : 'text-gray-900 hover:bg-blue-50',
88+ highlightedIndex === index ? 'bg-blue-100' : ''
89+ ]"
90+ :aria-selected =" isSelected(option)"
91+ role =" option"
92+ tabindex =" -1" >
93+ <!-- 如果是SVG字符串 -->
94+ <div v-if =" getSvgIcon(option)" v-html =" getSvgIcon(option)" class =" w-6 h-6 flex-shrink-0" />
95+
96+ <!-- 如果是SVG URL -->
97+ <img v-else-if =" getSvgUrl(option)" :src =" getSvgUrl(option)" class =" w-6 h-6 flex-shrink-0" alt =" icon" />
98+
99+ <span :class =" ['block truncate flex-1', isSelected(option) ? 'font-medium' : 'font-normal']" >
100+ {{ getOptionLabel(option) }}
101+ </span >
102+
103+ <!-- 选中图标 -->
104+ <span v-if =" isSelected(option)"
105+ class =" flex-shrink-0 ml-2" >
106+ <CheckIcon class =" h-5 w-5" aria-hidden =" true" />
107+ </span >
108+ </div >
109+ </template >
110+
111+ <!-- 无选项提示 -->
112+ <div v-else class =" px-3 py-2 text-gray-500 text-sm" >
113+ {{ noOptionsText }}
104114 </div >
105- </template >
106-
107- <!-- 无选项提示 -->
108- <div v-else class =" px-3 py-2 text-gray-500 text-sm" >
109- {{ noOptionsText }}
110115 </div >
111116 </div >
112117 </Transition >
@@ -229,28 +234,57 @@ const updateDropdownPosition = async () => {
229234
230235 const buttonRect = selectButton .value .getBoundingClientRect ()
231236 const viewportHeight = window .innerHeight
237+ const viewportWidth = window .innerWidth
232238
233239 // 计算下方可用空间
234- const spaceBelow = viewportHeight - buttonRect .bottom
235- const dropdownHeight = 240 // max-h-60 对应约240px
240+ const spaceBelow = viewportHeight - buttonRect .bottom - 10 // 留10px边距
241+ const spaceAbove = buttonRect . top - 10 // 留10px边距
236242
237- let top = buttonRect .bottom + 2 // 默认显示在下方
238- let left = buttonRect .left
239- let width = buttonRect .width
243+ // 搜索框高度(如果启用)
244+ const searchBoxHeight = props .searchable ? 60 : 0
245+
246+ // 基础下拉框高度
247+ const baseDropdownHeight = 240 // max-h-60 对应约240px
248+ const totalDropdownHeight = baseDropdownHeight + searchBoxHeight
240249
241- // 如果下方空间不足,显示在上方
242- if (spaceBelow < dropdownHeight && buttonRect .top > dropdownHeight ) {
243- // 显示在上方时,让下拉框紧贴按钮顶部
244- top = buttonRect .top - 2
250+ let top = buttonRect .bottom + 4 // 默认显示在下方,留4px间距
251+ let left = Math .max (10 , Math .min (buttonRect .left , viewportWidth - buttonRect .width - 10 )) // 确保不超出视口
252+ let width = buttonRect .width
253+ let maxHeight = Math .min (totalDropdownHeight , spaceBelow )
254+
255+ // 如果下方空间不足且上方空间更大,显示在上方
256+ if (spaceBelow < 150 && spaceAbove > spaceBelow ) {
257+ top = buttonRect .top - 4 // 显示在上方,留4px间距
258+ maxHeight = Math .min (totalDropdownHeight , spaceAbove )
259+
260+ dropdownStyle .value = {
261+ top: ` ${ top }px ` ,
262+ left: ` ${ left }px ` ,
263+ width: ` ${ width }px ` ,
264+ minWidth: ` ${ width }px ` ,
265+ maxHeight: ` ${ maxHeight }px ` ,
266+ transform: ' translateY(-100%)'
267+ }
268+ }
269+ else {
270+ dropdownStyle .value = {
271+ top: ` ${ top }px ` ,
272+ left: ` ${ left }px ` ,
273+ width: ` ${ width }px ` ,
274+ minWidth: ` ${ width }px ` ,
275+ maxHeight: ` ${ maxHeight }px ` ,
276+ transform: ' none'
277+ }
245278 }
279+ }
246280
247- dropdownStyle .value = {
248- top: ` ${ top }px ` ,
249- left: ` ${ left }px ` ,
250- width: ` ${ width }px ` ,
251- minWidth: ` ${ width }px ` ,
252- transform: spaceBelow < dropdownHeight && buttonRect .top > dropdownHeight ? ' translateY(-100%)' : ' none'
281+ // 防抖更新位置
282+ let updateTimer: number | null = null
283+ const debouncedUpdatePosition = () => {
284+ if (updateTimer ) {
285+ clearTimeout (updateTimer )
253286 }
287+ updateTimer = window .setTimeout (updateDropdownPosition , 10 )
254288}
255289
256290// 方法
@@ -287,9 +321,9 @@ const openDropdown = async () => {
287321 // 更新位置
288322 await updateDropdownPosition ()
289323
290- // 监听滚动和窗口大小变化
291- window .addEventListener (' scroll' , updateDropdownPosition , true )
292- window .addEventListener (' resize' , updateDropdownPosition )
324+ // 监听滚动和窗口大小变化,使用防抖
325+ window .addEventListener (' scroll' , debouncedUpdatePosition , true )
326+ window .addEventListener (' resize' , debouncedUpdatePosition )
293327
294328 if (props .searchable ) {
295329 nextTick (() => {
@@ -305,9 +339,15 @@ const closeDropdown = () => {
305339 searchQuery .value = ' '
306340 highlightedIndex .value = - 1
307341
342+ // 清理定时器
343+ if (updateTimer ) {
344+ clearTimeout (updateTimer )
345+ updateTimer = null
346+ }
347+
308348 // 移除监听器
309- window .removeEventListener (' scroll' , updateDropdownPosition , true )
310- window .removeEventListener (' resize' , updateDropdownPosition )
349+ window .removeEventListener (' scroll' , debouncedUpdatePosition , true )
350+ window .removeEventListener (' resize' , debouncedUpdatePosition )
311351}
312352
313353const selectOption = (option : Option ) => {
@@ -382,9 +422,14 @@ onMounted(() => {
382422})
383423
384424onUnmounted (() => {
425+ // 清理定时器
426+ if (updateTimer ) {
427+ clearTimeout (updateTimer )
428+ }
429+
385430 document .removeEventListener (' click' , handleClickOutside )
386431 document .removeEventListener (' keydown' , handleKeydown )
387- window .removeEventListener (' scroll' , updateDropdownPosition , true )
388- window .removeEventListener (' resize' , updateDropdownPosition )
432+ window .removeEventListener (' scroll' , debouncedUpdatePosition , true )
433+ window .removeEventListener (' resize' , debouncedUpdatePosition )
389434})
390435 </script >
0 commit comments