添加 Toast 通知功能,优化鼠标悬浮和键盘导航逻辑,增强用户交互体验。临时禁用鼠标悬浮功能以避免误操作,并在列表边界触发时提供反馈。

This commit is contained in:
2024-12-11 14:44:18 +08:00
parent e1872b5dca
commit 68da52bbb5
2 changed files with 148 additions and 27 deletions

View File

@@ -54,11 +54,22 @@ const showContextMenu = (data) => {
listFrozen.value = true // 冻结列表 listFrozen.value = true // 冻结列表
} }
// 添加一个状态来控制临时禁用鼠标悬浮
const temporaryDisableHover = ref(false)
let hoverDisableTimer = null
// 修改关闭菜单的处理 // 修改关闭菜单的处理
const closeMenu = () => { const closeMenu = () => {
showMenu.value = false showMenu.value = false
listFrozen.value = false listFrozen.value = false
selectedMenuIndex.value = -1 // 重置菜单选中项 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) { if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
e.preventDefault() e.preventDefault()
handleMenuTriggerClick(e) handleMenuTriggerClick(e)
} else if (e.key === 'ArrowLeft' && showMenu.value) { } else if ((e.key === 'ArrowLeft' || e.key === 'Escape') && showMenu.value) {
e.preventDefault() e.preventDefault()
showMenu.value = false closeMenu()
listFrozen.value = false
selectedMenuIndex.value = -1 // 重置菜单选中项
} else if (e.key === 'Escape') {
// ESC 键只在菜单打开时响应
if (showMenu.value) {
e.preventDefault()
closeMenu()
}
} else if (showMenu.value) { } else if (showMenu.value) {
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
@@ -108,7 +111,7 @@ const handleKeyDown = (e) => {
} }
onMounted(() => { onMounted(() => {
// 添加全局击事件来关闭菜单 // 添加全局击事件来关闭菜单
document.addEventListener('click', closeMenu) document.addEventListener('click', closeMenu)
// 添加键盘事件监听 // 添加键盘事件监听
document.addEventListener('keydown', handleKeyDown) document.addEventListener('keydown', handleKeyDown)
@@ -118,6 +121,12 @@ onBeforeUnmount(() => {
document.removeEventListener('click', closeMenu) document.removeEventListener('click', closeMenu)
// 移除键盘事件监听 // 移除键盘事件监听
document.removeEventListener('keydown', handleKeyDown) document.removeEventListener('keydown', handleKeyDown)
if (toastTimer) {
clearTimeout(toastTimer)
}
if (hoverDisableTimer) {
clearTimeout(hoverDisableTimer)
}
}) })
// 修改菜单击处理 // 修改菜单击处理
@@ -157,6 +166,32 @@ const onAfterLeave = (el) => {
// 过渡结束后重置样式 // 过渡结束后重置样式
el.style.display = '' 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)
}
})
</script> </script>
<template> <template>
@@ -166,9 +201,11 @@ const onAfterLeave = (el) => {
:data="listData" :data="listData"
:config="listConfig" :config="listConfig"
:frozen="listFrozen" :frozen="listFrozen"
:disable-hover="showMenu || temporaryDisableHover"
@select="handleSelect" @select="handleSelect"
@click="handleClick" @click="handleClick"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
@showToast="showToast"
/> />
</div> </div>
<div class="toolbar"> <div class="toolbar">
@@ -211,6 +248,12 @@ const onAfterLeave = (el) => {
</div> </div>
</div> </div>
</div> </div>
<!-- 添加 Toast 组件 -->
<transition name="toast">
<div v-if="toastVisible" class="toast">
{{ toastMessage }}
</div>
</transition>
</div> </div>
</template> </template>
@@ -365,4 +408,32 @@ body {
word-break: break-all; word-break: break-all;
line-height: 1.5; 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%);
}
</style> </style>

View File

@@ -25,10 +25,14 @@ const props = defineProps({
frozen: { frozen: {
type: Boolean, type: Boolean,
default: false default: false
},
disableHover: {
type: Boolean,
default: false
} }
}) })
const emit = defineEmits(['select', 'click', 'contextmenu']) const emit = defineEmits(['select', 'click', 'contextmenu', 'showToast'])
// 计算列表项实际总高度 // 计算列表项实际总高度
const totalItemHeight = computed(() => const totalItemHeight = computed(() =>
@@ -133,9 +137,13 @@ const handleScroll = debounce(() => {
} }
}, props.config.scrollDebounceTime) }, props.config.scrollDebounceTime)
// 鼠标移入处理 // 添加一个状态来控制是否禁用鼠标悬浮
const disableHover = ref(false)
let hoverTimer = null
// 修改鼠标移入处理函数
function handleMouseEnter(index) { function handleMouseEnter(index) {
if (props.frozen) return // 如果列表被冻结,不响应鼠标移入 if (props.frozen || disableHover.value || props.disableHover) return // 增加 props.disableHover 的判断
if (!isKeyboardNavigating.value) { if (!isKeyboardNavigating.value) {
selectedIndex.value = index selectedIndex.value = index
emit('select', { ...props.data[index], index }) emit('select', { ...props.data[index], index })
@@ -143,11 +151,17 @@ function handleMouseEnter(index) {
} }
} }
// 键盘事件处理 // 添加记录上次到达边界的时间
const lastReachBoundaryTime = ref({
top: 0,
bottom: 0
})
// 修改键盘事件处理函数
function handleKeyDown(e) { function handleKeyDown(e) {
if (props.frozen) return // 如果列表被冻结,不响应键盘事件 if (props.frozen) return
if (e.key === 'Enter') { if (e.key === 'Enter') {
// 回车键触发点击事件
if (selectedIndex.value >= 0 && selectedIndex.value < props.data.length) { if (selectedIndex.value >= 0 && selectedIndex.value < props.data.length) {
const item = props.data[selectedIndex.value] const item = props.data[selectedIndex.value]
emit('click', { ...item, index: selectedIndex.value }) emit('click', { ...item, index: selectedIndex.value })
@@ -157,26 +171,59 @@ function handleKeyDown(e) {
const isFirstItem = selectedIndex.value === 0 const isFirstItem = selectedIndex.value === 0
const isLastItem = selectedIndex.value === props.data.length - 1 const isLastItem = selectedIndex.value === props.data.length - 1
const now = Date.now()
// 如果已经在第一项还按上键,或在最后一项还按下键,则不处理 if (e.key === 'ArrowUp' && isFirstItem) {
if ((e.key === 'ArrowUp' && isFirstItem) || const timeSinceLastTop = now - lastReachBoundaryTime.value.top
(e.key === 'ArrowDown' && isLastItem)) { 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 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 isKeyboardNavigating.value = true
if (keyboardTimer) clearTimeout(keyboardTimer) if (keyboardTimer) clearTimeout(keyboardTimer)
const containerTop = containerRef.value?.scrollTop || 0
const containerHeight = containerRef.value?.clientHeight || 0
if (e.key === 'ArrowUp') { if (e.key === 'ArrowUp') {
const nextIndex = Math.max(0, selectedIndex.value - 1) const nextIndex = Math.max(0, selectedIndex.value - 1)
selectedIndex.value = nextIndex selectedIndex.value = nextIndex
emit('select', { ...props.data[nextIndex], index: nextIndex }) emit('select', { ...props.data[nextIndex], index: nextIndex })
const itemTop = nextIndex * totalItemHeight.value const itemTop = nextIndex * totalItemHeight.value
if (itemTop < containerTop && containerRef.value) { if (itemTop < containerRef.value?.scrollTop && containerRef.value) {
containerRef.value.scrollTop = itemTop containerRef.value.scrollTop = itemTop
} }
} else { } else {
@@ -185,9 +232,9 @@ function handleKeyDown(e) {
emit('select', { ...props.data[nextIndex], index: nextIndex }) emit('select', { ...props.data[nextIndex], index: nextIndex })
const itemBottom = (nextIndex + 1) * totalItemHeight.value const itemBottom = (nextIndex + 1) * totalItemHeight.value
const scrollBottom = containerTop + containerHeight const scrollBottom = (containerRef.value?.scrollTop || 0) + containerHeight.value
if (itemBottom > scrollBottom && containerRef.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() { function ensureSelectedItemVisible() {
if (!containerRef.value) return if (!containerRef.value) return
@@ -259,6 +306,9 @@ onBeforeUnmount(() => {
if (keyboardTimer) { if (keyboardTimer) {
clearTimeout(keyboardTimer) clearTimeout(keyboardTimer)
} }
if (hoverTimer) {
clearTimeout(hoverTimer)
}
}) })
</script> </script>