diff --git a/src/App.vue b/src/App.vue index d7cf926..33b2986 100644 --- a/src/App.vue +++ b/src/App.vue @@ -54,11 +54,22 @@ const showContextMenu = (data) => { listFrozen.value = true // 冻结列表 } +// 添加一个状态来控制临时禁用鼠标悬浮 +const temporaryDisableHover = ref(false) +let hoverDisableTimer = null + // 修改关闭菜单的处理 const closeMenu = () => { showMenu.value = false listFrozen.value = false selectedMenuIndex.value = -1 // 重置菜单选中项 + + // 临时禁用鼠标悬浮 + temporaryDisableHover.value = true + if (hoverDisableTimer) clearTimeout(hoverDisableTimer) + hoverDisableTimer = setTimeout(() => { + temporaryDisableHover.value = false + }, 300) // 300ms 后恢复鼠标悬浮功能 } // 添加当前选中的菜单项索引 @@ -69,17 +80,9 @@ const handleKeyDown = (e) => { if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) { e.preventDefault() handleMenuTriggerClick(e) - } else if (e.key === 'ArrowLeft' && showMenu.value) { + } else if ((e.key === 'ArrowLeft' || e.key === 'Escape') && showMenu.value) { e.preventDefault() - showMenu.value = false - listFrozen.value = false - selectedMenuIndex.value = -1 // 重置菜单选中项 - } else if (e.key === 'Escape') { - // ESC 键只在菜单打开时响应 - if (showMenu.value) { - e.preventDefault() - closeMenu() - } + closeMenu() } else if (showMenu.value) { switch (e.key) { case 'ArrowUp': @@ -108,7 +111,7 @@ const handleKeyDown = (e) => { } onMounted(() => { - // 添加全局点击事件来关闭菜单 + // 添加全局击事件来关闭菜单 document.addEventListener('click', closeMenu) // 添加键盘事件监听 document.addEventListener('keydown', handleKeyDown) @@ -118,6 +121,12 @@ onBeforeUnmount(() => { document.removeEventListener('click', closeMenu) // 移除键盘事件监听 document.removeEventListener('keydown', handleKeyDown) + if (toastTimer) { + clearTimeout(toastTimer) + } + if (hoverDisableTimer) { + clearTimeout(hoverDisableTimer) + } }) // 修改菜单击处理 @@ -157,6 +166,32 @@ const onAfterLeave = (el) => { // 过渡结束后重置样式 el.style.display = '' } + +// 添加 toast 相关状态 +const toastVisible = ref(false) +const toastMessage = ref('') +let toastTimer = null + +// 添加 toast 处理函数 +const showToast = (message) => { + if (toastTimer) { + clearTimeout(toastTimer) + } + + toastMessage.value = message + toastVisible.value = true + + toastTimer = setTimeout(() => { + toastVisible.value = false + }, 3000) +} + +// 在 onBeforeUnmount 中清理定时器 +onBeforeUnmount(() => { + if (toastTimer) { + clearTimeout(toastTimer) + } +}) @@ -365,4 +408,32 @@ body { word-break: break-all; line-height: 1.5; } + +/* 添加 Toast 相关样式 */ +.toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + pointer-events: none; + max-width: 80%; + text-align: center; +} + +.toast-enter-active, +.toast-leave-active { + transition: all 0.3s ease; +} + +.toast-enter-from, +.toast-leave-to { + opacity: 0; + transform: translate(-50%, -40%); +} diff --git a/src/components/VirtualList.vue b/src/components/VirtualList.vue index f6bea0e..1cb0b64 100644 --- a/src/components/VirtualList.vue +++ b/src/components/VirtualList.vue @@ -25,10 +25,14 @@ const props = defineProps({ frozen: { type: Boolean, default: false + }, + disableHover: { + type: Boolean, + default: false } }) -const emit = defineEmits(['select', 'click', 'contextmenu']) +const emit = defineEmits(['select', 'click', 'contextmenu', 'showToast']) // 计算列表项实际总高度 const totalItemHeight = computed(() => @@ -133,9 +137,13 @@ const handleScroll = debounce(() => { } }, props.config.scrollDebounceTime) -// 鼠标移入处理 +// 添加一个状态来控制是否禁用鼠标悬浮 +const disableHover = ref(false) +let hoverTimer = null + +// 修改鼠标移入处理函数 function handleMouseEnter(index) { - if (props.frozen) return // 如果列表被冻结,不响应鼠标移入 + if (props.frozen || disableHover.value || props.disableHover) return // 增加 props.disableHover 的判断 if (!isKeyboardNavigating.value) { selectedIndex.value = index emit('select', { ...props.data[index], index }) @@ -143,11 +151,17 @@ function handleMouseEnter(index) { } } -// 键盘事件处理 +// 添加记录上次到达边界的时间 +const lastReachBoundaryTime = ref({ + top: 0, + bottom: 0 +}) + +// 修改键盘事件处理函数 function handleKeyDown(e) { - if (props.frozen) return // 如果列表被冻结,不响应键盘事件 + if (props.frozen) return + if (e.key === 'Enter') { - // 回车键触发点击事件 if (selectedIndex.value >= 0 && selectedIndex.value < props.data.length) { const item = props.data[selectedIndex.value] emit('click', { ...item, index: selectedIndex.value }) @@ -157,26 +171,59 @@ function handleKeyDown(e) { const isFirstItem = selectedIndex.value === 0 const isLastItem = selectedIndex.value === props.data.length - 1 + const now = Date.now() - // 如果已经在第一项还按上键,或在最后一项还按下键,则不处理 - if ((e.key === 'ArrowUp' && isFirstItem) || - (e.key === 'ArrowDown' && isLastItem)) { + if (e.key === 'ArrowUp' && isFirstItem) { + const timeSinceLastTop = now - lastReachBoundaryTime.value.top + if (timeSinceLastTop < 5000) { + // 5秒内再次点击,跳转到底部 + disableHover.value = true // 禁用鼠标悬浮 + selectedIndex.value = props.data.length - 1 + emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value }) + ensureSelectedItemVisible() + // 1秒后恢复鼠标悬浮功能 + if (hoverTimer) clearTimeout(hoverTimer) + hoverTimer = setTimeout(() => { + disableHover.value = false + }, 1000) + } else { + lastReachBoundaryTime.value.top = now + emit('showToast', '已经到列表顶端,再次点击将回到列表底端') + } return } + if (e.key === 'ArrowDown' && isLastItem) { + const timeSinceLastBottom = now - lastReachBoundaryTime.value.bottom + if (timeSinceLastBottom < 5000) { + // 5秒内再次点击,跳转到顶部 + disableHover.value = true // 禁用鼠标悬浮 + selectedIndex.value = 0 + emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value }) + ensureSelectedItemVisible() + // 1秒后恢复鼠标悬浮功能 + if (hoverTimer) clearTimeout(hoverTimer) + hoverTimer = setTimeout(() => { + disableHover.value = false + }, 1000) + } else { + lastReachBoundaryTime.value.bottom = now + emit('showToast', '已经到列表底端,再次点击将回到列表顶端') + } + return + } + + // 原有的上下键处理逻辑 isKeyboardNavigating.value = true if (keyboardTimer) clearTimeout(keyboardTimer) - const containerTop = containerRef.value?.scrollTop || 0 - const containerHeight = containerRef.value?.clientHeight || 0 - if (e.key === 'ArrowUp') { const nextIndex = Math.max(0, selectedIndex.value - 1) selectedIndex.value = nextIndex emit('select', { ...props.data[nextIndex], index: nextIndex }) const itemTop = nextIndex * totalItemHeight.value - if (itemTop < containerTop && containerRef.value) { + if (itemTop < containerRef.value?.scrollTop && containerRef.value) { containerRef.value.scrollTop = itemTop } } else { @@ -185,9 +232,9 @@ function handleKeyDown(e) { emit('select', { ...props.data[nextIndex], index: nextIndex }) const itemBottom = (nextIndex + 1) * totalItemHeight.value - const scrollBottom = containerTop + containerHeight + const scrollBottom = (containerRef.value?.scrollTop || 0) + containerHeight.value if (itemBottom > scrollBottom && containerRef.value) { - containerRef.value.scrollTop = itemBottom - containerHeight + containerRef.value.scrollTop = itemBottom - containerHeight.value } } @@ -197,7 +244,7 @@ function handleKeyDown(e) { } } -// 确保选中项可见 +// 确保选项可见 function ensureSelectedItemVisible() { if (!containerRef.value) return @@ -259,6 +306,9 @@ onBeforeUnmount(() => { if (keyboardTimer) { clearTimeout(keyboardTimer) } + if (hoverTimer) { + clearTimeout(hoverTimer) + } })