Files
Aether/frontend/src/composables/useEscapeKey.ts
fawney19 8c4c5fa394 feat(ui): 增加模型连接性测试功能并优化对话框 ESC 键处理
- 模型测试: 新增测试按钮,支持多 API 格式选择
- ESC 键: 重构为全局栈管理,支持嵌套对话框
- 修复: check_endpoint 使用 FORMAT_ID 替代 name
- 优化: Redis 连接池添加健康检查

Co-authored-by: htmambo <htmambo@users.noreply.github.com>
2026-01-13 20:11:09 +08:00

137 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { onMounted, onUnmounted, ref } from 'vue'
/**
* 全局对话框栈,用于管理嵌套对话框的 ESC 键处理顺序
* 栈顶的对话框优先处理 ESC 键
*/
const dialogStack = ref<Array<() => void | boolean>>([])
/**
* 全局 ESC 键监听器(只有一个)
*/
let globalListenerAttached = false
function globalEscapeHandler(event: KeyboardEvent) {
// 只处理 ESC 键
if (event.key !== 'Escape') return
// 从栈顶向栈底查找能处理 ESC 键的对话框
// 倒序遍历,先检查最后加入的(栈顶)
for (let i = dialogStack.value.length - 1; i >= 0; i--) {
const handler = dialogStack.value[i]
const handled = handler()
if (handled === true) {
// 该对话框已处理事件,阻止传播
event.stopPropagation()
event.stopImmediatePropagation()
return
}
}
}
/**
* ESC 键监听 Composable栈管理版本
* 用于按 ESC 键关闭弹窗或其他可关闭的组件
*
* 支持嵌套对话框场景:只有最上层的对话框(最后打开的)会响应 ESC 键
*
* @param callback - 按 ESC 键时执行的回调函数,返回 true 表示已处理事件
* @param options - 配置选项
*/
export function useEscapeKey(
callback: () => void | boolean,
options: {
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
disableOnInput?: boolean
/** 是否只监听一次,默认 false */
once?: boolean
} = {}
) {
const { disableOnInput = true, once = false } = options
const isActive = ref(true)
const isInStack = ref(false)
// 包装原始回调,添加输入框检查
function wrappedCallback(): void | boolean {
// 检查组件是否还活跃
if (!isActive.value) return false
// 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素
if (disableOnInput) {
const activeElement = document.activeElement
const isInputElement = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.contentEditable === 'true' ||
activeElement.getAttribute('role') === 'textbox' ||
activeElement.getAttribute('role') === 'combobox'
)
// 如果焦点在输入框中,不处理 ESC 键
if (isInputElement) return false
}
// 执行原始回调
const handled = callback()
// 如果只监听一次,则从栈中移除
if (once && handled === true) {
removeFromStack()
}
// 移除当前元素的焦点,避免残留样式
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
return handled
}
function addToStack() {
if (isInStack.value) return
// 将当前处理器加入栈顶
dialogStack.value.push(wrappedCallback)
isInStack.value = true
// 确保全局监听器已添加
if (!globalListenerAttached) {
document.addEventListener('keydown', globalEscapeHandler, true) // 使用捕获阶段
globalListenerAttached = true
}
}
function removeFromStack() {
if (!isInStack.value) return
// 从栈中移除当前处理器
const index = dialogStack.value.indexOf(wrappedCallback)
if (index > -1) {
dialogStack.value.splice(index, 1)
}
isInStack.value = false
// 如果栈为空,移除全局监听器
if (dialogStack.value.length === 0 && globalListenerAttached) {
document.removeEventListener('keydown', globalEscapeHandler, true)
globalListenerAttached = false
}
}
onMounted(() => {
addToStack()
})
onUnmounted(() => {
isActive.value = false
removeFromStack()
})
return {
addToStack,
removeFromStack
}
}