添加 Toast 通知功能,优化鼠标悬浮和键盘导航逻辑,增强用户交互体验。临时禁用鼠标悬浮功能以避免误操作,并在列表边界触发时提供反馈。
This commit is contained in:
91
src/App.vue
91
src/App.vue
@@ -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()
|
|
||||||
showMenu.value = false
|
|
||||||
listFrozen.value = false
|
|
||||||
selectedMenuIndex.value = -1 // 重置菜单选中项
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
// ESC 键只在菜单打开时响应
|
|
||||||
if (showMenu.value) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
closeMenu()
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user