diff --git a/frontend/src/composables/useEscapeKey.ts b/frontend/src/composables/useEscapeKey.ts index 84604a6..f168c24 100644 --- a/frontend/src/composables/useEscapeKey.ts +++ b/frontend/src/composables/useEscapeKey.ts @@ -1,10 +1,42 @@ import { onMounted, onUnmounted, ref } from 'vue' /** - * ESC 键监听 Composable(简化版本,直接使用独立监听器) + * 全局对话框栈,用于管理嵌套对话框的 ESC 键处理顺序 + * 栈顶的对话框优先处理 ESC 键 + */ +const dialogStack = ref 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 键关闭弹窗或其他可关闭的组件 * - * @param callback - 按 ESC 键时执行的回调函数,返回 true 表示已处理事件,阻止其他监听器执行 + * 支持嵌套对话框场景:只有最上层的对话框(最后打开的)会响应 ESC 键 + * + * @param callback - 按 ESC 键时执行的回调函数,返回 true 表示已处理事件 * @param options - 配置选项 */ export function useEscapeKey( @@ -18,13 +50,12 @@ export function useEscapeKey( ) { const { disableOnInput = true, once = false } = options const isActive = ref(true) + const isInStack = ref(false) - function handleKeyDown(event: KeyboardEvent) { - // 只处理 ESC 键 - if (event.key !== 'Escape') return - + // 包装原始回调,添加输入框检查 + function wrappedCallback(): void | boolean { // 检查组件是否还活跃 - if (!isActive.value) return + if (!isActive.value) return false // 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素 if (disableOnInput) { @@ -39,13 +70,15 @@ export function useEscapeKey( ) // 如果焦点在输入框中,不处理 ESC 键 - if (isInputElement) return + if (isInputElement) return false } - // 执行回调,如果返回 true 则阻止其他监听器 + // 执行原始回调 const handled = callback() - if (handled === true) { - event.stopImmediatePropagation() + + // 如果只监听一次,则从栈中移除 + if (once && handled === true) { + removeFromStack() } // 移除当前元素的焦点,避免残留样式 @@ -53,31 +86,51 @@ export function useEscapeKey( document.activeElement.blur() } - // 如果只监听一次,则移除监听器 - if (once) { - removeEventListener() + 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 addEventListener() { - document.addEventListener('keydown', handleKeyDown) - } + function removeFromStack() { + if (!isInStack.value) return - function removeEventListener() { - document.removeEventListener('keydown', handleKeyDown) + // 从栈中移除当前处理器 + 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(() => { - addEventListener() + addToStack() }) onUnmounted(() => { isActive.value = false - removeEventListener() + removeFromStack() }) return { - addEventListener, - removeEventListener + addToStack, + removeFromStack } -} \ No newline at end of file +} diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index edd1a0d..7353454 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -374,6 +374,7 @@ v-if="provider" :key="`models-${provider.id}`" :provider="provider" + :endpoints="endpoints" @edit-model="handleEditModel" @delete-model="handleDeleteModel" @batch-assign="handleBatchAssign" diff --git a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue index 90b31c6..2e9517c 100644 --- a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue +++ b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue @@ -109,14 +109,42 @@ ${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次 - + -
+
+ +
+ + +
+ +
+