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 @@