diff --git a/static/src/app/bootstrap.ts b/static/src/app/bootstrap.ts index e2e1520e..eef78d62 100644 --- a/static/src/app/bootstrap.ts +++ b/static/src/app/bootstrap.ts @@ -1475,8 +1475,11 @@ function dispatchShowFileDownload(path: string) { }) .catch((err) => { console.warn('[show_file] 下载失败:', err); - // 兜底:直接跳转 - window.open(url, '_blank'); + // 失败不再兜底跳转(window.open 会把后端 JSON 错误整页展示),改为错误弹窗提示 + useUiStore().pushToast({ + message: err?.message || t('common.downloadFailed'), + type: 'error' + }); }); } diff --git a/static/src/components/chat/quickdock/FileWindow.vue b/static/src/components/chat/quickdock/FileWindow.vue index efb5ff83..b7ad2875 100644 --- a/static/src/components/chat/quickdock/FileWindow.vue +++ b/static/src/components/chat/quickdock/FileWindow.vue @@ -78,6 +78,7 @@ function openMenu(e: MouseEvent, row: Row) { key: row.path, left: rect.left, // 与按钮左对齐、向下展开 top: rect.bottom + 6, + btnTop: rect.top, alignRight: false }); } diff --git a/static/src/components/chat/quickdock/QuickDock.vue b/static/src/components/chat/quickdock/QuickDock.vue index 8778dcdc..07f23e5f 100644 --- a/static/src/components/chat/quickdock/QuickDock.vue +++ b/static/src/components/chat/quickdock/QuickDock.vue @@ -17,25 +17,34 @@ - - + + +
+ + +
+
@@ -46,7 +55,8 @@ import { t } from '@/locales'; import { useQuickDockStore, persistQuickDockHadContent, - persistQuickDockConvContent + persistQuickDockConvContent, + type QuickDockMenuState } from '@/stores/quickDock'; import { useSubAgentStore } from '@/stores/subAgent'; import { useBackgroundCommandStore } from '@/stores/backgroundCommand'; @@ -67,7 +77,7 @@ import FileWindow from './FileWindow.vue'; * 同时负责:全局 ⋯ 菜单、Esc 分层关闭、列表轮询、对话切换重置。 */ -defineProps<{ hostMode: boolean }>(); +const props = defineProps<{ hostMode: boolean }>(); const quickDock = useQuickDockStore(); const subAgentStore = useSubAgentStore(); @@ -243,14 +253,57 @@ const CMD_TERMINAL = new Set(['completed', 'failed', 'timeout', 'cancelled']); /* ---------------- 菜单定位与目标状态 ---------------- */ -const MENU_ESTIMATED_HEIGHT = 128; +const MENU_ITEM_HEIGHT = 30; +const MENU_CHROME = 10; // 上下 padding 4×2 + border 1×2 +const MENU_GAP = 6; +const VIEWPORT_MARGIN = 8; + +/** 离开动画期间沿用的最后一份菜单快照(menu 关闭即 null,直接读会让位置/内容在动画中跳变) */ +const lastMenu = ref(null); +watch( + menu, + (val) => { + if (val) { + lastMenu.value = val; + } + }, + { immediate: true } +); +const effectiveMenu = computed(() => menu.value || lastMenu.value); + +/** 菜单高度估算:行高固定 30px,按类型精确到项数,翻转时才能贴住按钮 */ +const menuEstimatedHeight = computed(() => { + const m = effectiveMenu.value; + if (!m) { + return 0; + } + if (m.type === 'runner') { + return MENU_ITEM_HEIGHT + MENU_CHROME; + } + return (props.hostMode ? 3 : 2) * MENU_ITEM_HEIGHT + MENU_CHROME; +}); + +/** 菜单是否翻转到按钮上方展开(下方放不下且上方更宽裕); + * 驱动定位与动画方向(qd-menu--above:从下到上淡入) */ +const menuFlipped = computed(() => { + const m = effectiveMenu.value; + if (!m) { + return false; + } + const spaceBelow = window.innerHeight - VIEWPORT_MARGIN - m.top; + const spaceAbove = m.btnTop - MENU_GAP - VIEWPORT_MARGIN; + return spaceBelow < menuEstimatedHeight.value && spaceAbove > spaceBelow; +}); const menuStyle = computed(() => { - const m = menu.value; + const m = effectiveMenu.value; if (!m) { return {}; } - const top = Math.min(m.top, window.innerHeight - MENU_ESTIMATED_HEIGHT - 8); + const estHeight = menuEstimatedHeight.value; + const top = menuFlipped.value + ? Math.max(VIEWPORT_MARGIN, m.btnTop - MENU_GAP - estHeight) + : Math.min(m.top, window.innerHeight - estHeight - VIEWPORT_MARGIN); if (m.alignRight) { return { right: `${window.innerWidth - m.left}px`, top: `${top}px` }; } diff --git a/static/src/components/chat/quickdock/RunnerDetailPanel.vue b/static/src/components/chat/quickdock/RunnerDetailPanel.vue index 2b1c6ae9..4c10aac1 100644 --- a/static/src/components/chat/quickdock/RunnerDetailPanel.vue +++ b/static/src/components/chat/quickdock/RunnerDetailPanel.vue @@ -1,8 +1,10 @@