From f2676caa80aeeb578b4c736c1880b0ce868cc094 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 9 Sep 2026 18:09:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E5=BF=AB=E6=8D=B7=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=BF=BB=E8=BD=AC=E5=8A=A8=E7=94=BB=E4=B8=8E=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=9D=A2=E6=9D=BF=E4=BF=AE=E5=A4=8D=EF=BC=8Cmd=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=93=BE=E6=8E=A5=E7=BB=9F=E4=B8=80=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuickDock ⋯ 菜单:下方空间不足时翻转到按钮上方展开;进出动画按 翻转状态分方向(下方展开从上到下、翻转到上方时从下到上),补离开动画 - 子智能体详情面板:补 CloseButton 导入修复关闭按钮缺失;right 定位 改为实时跟随快捷栏实际位置(右侧开预览/终端面板时不再盖住按钮) - md 相对路径链接识别为工作区文件链接,与 download:// 统一走 /api/download/file 下载(web blob / Android 原生桥接);修复中文路径 因 micromark 规范化编码未解码导致的双重编码 404 - 文件链接样式重构:文件图标 + 中性文字 + 虚线下划线(对齐 ChatGPT), 图标跟随文字色、下缘对齐虚线,行高 2.5;下载失败改弹 toast 不再跳转错误页 Co-authored-by: Astrion powered by Kimi-K3 --- static/src/app/bootstrap.ts | 7 +- .../components/chat/quickdock/FileWindow.vue | 1 + .../components/chat/quickdock/QuickDock.vue | 101 +++++++++++++----- .../chat/quickdock/RunnerDetailPanel.vue | 34 +++++- .../chat/quickdock/RunnerWindow.vue | 1 + .../components/chat/quickdock/quickdock.css | 50 ++++++--- static/src/composables/useMarkdownRenderer.ts | 60 +++++++++-- static/src/stores/quickDock.ts | 4 +- .../styles/components/chat/_chat-area.scss | 38 ++++++- 9 files changed, 242 insertions(+), 54 deletions(-) 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 @@