From b3158913ef3a7391f2d0378994e991d78b63ae85 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 2 Sep 2026 01:07:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E5=86=85=E5=AE=B9=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=85=A8=E7=AB=99=E6=8E=A5=E5=85=A5=E7=81=AF=E7=AE=B1?= =?UTF-8?q?=E9=A2=84=E8=A7=88=EF=BC=8C=E5=85=B3=E9=97=AD=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=BA=20CloseButton=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 document 级事件委托(bootstrap.ts setupImagePreviewDelegation), show_image 卡片 / Markdown 正文图 / view_image / ocr_image 工具结果 统一支持点击灯箱预览;可预览图片加 zoom-in 光标 - show_file 图片卡片由打开新标签页改为灯箱预览,并解除 Android 端禁用 - CitationPopover 引用缩略图支持点击预览 - renderShowFileCard 独立 app 实例补挂 i18n 插件(修复 $t 不可用隐患) - 新建 common/CloseButton.vue(boxed 圆角方 hover 底 / bare 裸字形双变体), 替换全站 25 处形态各异的关闭按钮(圆形/圆角方/裸字符/macOS 红点等 8 种风格), 修复 ImagePicker/VideoPicker/路径授权弹窗原本无 hover 反馈的问题 - showHtmlFullscreen 顶栏按钮 CSS 对齐 boxed 形态(动态 DOM 无法复用组件) - 清理 10+ 份散落重复的关闭按钮旧样式与 .mobile-overlay-close 死代码 Co-authored-by: Astrion powered by Kimi-K3 --- static/src/admin/PolicyApp.vue | 11 +- static/src/app/bootstrap.ts | 49 ++++++- static/src/app/lifecycle.ts | 3 +- .../src/components/chat/CitationPopover.vue | 15 +- .../src/components/chat/EditSummaryCard.vue | 32 +---- static/src/components/chat/ShowFileCard.vue | 7 +- .../components/chat/actions/toolRenderers.ts | 4 +- .../chat/quickdock/FilePreviewPanel.vue | 12 +- .../chat/quickdock/RunnerDetailPanel.vue | 13 +- .../components/chat/quickdock/quickdock.css | 50 ------- static/src/components/common/CloseButton.vue | 131 ++++++++++++++++++ .../overlay/BackgroundCommandDialog.vue | 3 +- .../overlay/ConversationReviewDialog.vue | 40 +----- .../components/overlay/GoalProgressDialog.vue | 3 +- .../src/components/overlay/ImageLightbox.vue | 34 ++--- static/src/components/overlay/ImagePicker.vue | 10 +- .../overlay/PathAuthorizationDialog.vue | 5 +- .../components/overlay/PlanApprovalDialog.vue | 32 ++--- .../components/overlay/SandboxSetupDialog.vue | 28 +--- .../overlay/SubAgentActivityDialog.vue | 3 +- .../components/overlay/UserQuestionDialog.vue | 16 +-- .../components/overlay/VersioningDialog.vue | 25 +--- static/src/components/overlay/VideoPicker.vue | 10 +- static/src/components/panels/FocusPanel.vue | 11 +- .../src/components/panels/GitChangesPanel.vue | 5 +- .../src/components/panels/TerminalPanel.vue | 31 +---- .../components/panels/ToolApprovalPanel.vue | 5 +- .../personalization/PersonalizationDrawer.vue | 29 ++-- .../styles/settings-shared.css | 25 ---- static/src/components/shell/QuotaToast.vue | 3 +- static/src/components/shell/ToastStack.vue | 12 +- static/src/components/token/TokenDrawer.vue | 10 +- .../components/workflow/WorkflowDemoShell.vue | 3 +- .../styles/components/chat/_chat-area.scss | 28 +++- .../styles/components/input/_composer.scss | 9 -- .../styles/components/overlays/_overlays.scss | 29 +--- .../components/panels/_resource-panel.scss | 34 +---- .../components/panels/_right-panel.scss | 55 -------- 38 files changed, 317 insertions(+), 508 deletions(-) create mode 100644 static/src/components/common/CloseButton.vue diff --git a/static/src/admin/PolicyApp.vue b/static/src/admin/PolicyApp.vue index 6f305302..906f6b19 100644 --- a/static/src/admin/PolicyApp.vue +++ b/static/src/admin/PolicyApp.vue @@ -49,7 +49,7 @@ @@ -300,6 +300,7 @@ import { computed, reactive, ref, onMounted, onBeforeUnmount, watch } from 'vue' import { useSecondaryPass } from './useSecondaryPass'; import SecondaryGate from './SecondaryGate.vue'; import FancyCheck from '@/components/common/FancyCheck.vue'; +import CloseButton from '@/components/common/CloseButton.vue'; import { t, currentLocale } from '@/locales'; type TargetType = 'global' | 'role' | 'user' | 'invite'; @@ -1061,14 +1062,6 @@ onBeforeUnmount(() => { border-color: rgba(189, 93, 58, 0.35); } -.banner-close { - border: none; - background: transparent; - cursor: pointer; - font-size: 16px; - line-height: 1; -} - .panel { background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(118, 103, 84, 0.2); diff --git a/static/src/app/bootstrap.ts b/static/src/app/bootstrap.ts index 1cb7ffba..e2e1520e 100644 --- a/static/src/app/bootstrap.ts +++ b/static/src/app/bootstrap.ts @@ -2,8 +2,9 @@ import katex from 'katex'; import DOMPurify from 'dompurify'; import { createApp } from 'vue'; -import { t } from '@/locales'; +import { t, i18n } from '@/locales'; import ShowFileCard from '../components/chat/ShowFileCard.vue'; +import { useUiStore } from '@/stores/ui'; import { buildShowHtmlIframeSrcdoc } from '../utils/showHtmlSandbox'; import { openShowHtmlFullscreen, @@ -174,10 +175,15 @@ function normalizeShowImageSrc(src: string) { trimmed = trimmed.replace(/^\/(?=[A-Za-z]:\/)/, ''); } if (trimmed.startsWith('/user_upload/')) return trimmed; - // 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx - const idx = trimmed.toLowerCase().indexOf('/user_upload/'); - if (idx >= 0) { - return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length); + // 兼容容器内部绝对路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx + // 注意:.astrion/user_upload/xxx 是工作区相对路径,不在此特判, + // 让它落到下方通用分支走 /api/file/content(与 _validate_path 同源解析, + // 避免依赖 /user_upload/ 后端路由在宿主机免登录模式下的工作区解析) + if (trimmed.startsWith('/')) { + const idx = trimmed.toLowerCase().indexOf('/user_upload/'); + if (idx >= 0) { + return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length); + } } // /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理 if (trimmed === '/workspace') return ''; @@ -1256,6 +1262,36 @@ function buildNodePathKey(node: Element) { return parts.reverse().join('/'); } +// ===== 内容图片点击预览(document 级事件委托) ===== +// 覆盖三类无 Vue 事件绑定的注入图片: +// 1. 卡片(figure.chat-inline-image,renderShowImages 动态创建) +// 2. Markdown ![]() 正文图片(.text-content 内的 img) +// 3. 工具结果图片(.tool-result-image,如 view_image / ocr_image) +// 不会误伤:消息气泡图片与 CitationPopover 缩略图已用 @click.stop 阻断冒泡; +// ShowFileCard 卡片自带 @click,在此显式排除;头像/图标为 SVG 或不在命中容器内。 +let imagePreviewDelegationBound = false; + +export function setupImagePreviewDelegation() { + if (imagePreviewDelegationBound) return; + imagePreviewDelegationBound = true; + document.addEventListener('click', (event) => { + const target = event.target as HTMLElement | null; + if (!target || typeof target.closest !== 'function') return; + const img = target.closest('img') as HTMLImageElement | null; + if (!img) return; + // ShowFileCard 内部已自行打开灯箱预览,避免双重触发 + if (img.closest('.show-file-card')) return; + const hit = + img.closest('figure.chat-inline-image') || + img.closest('.tool-result-image') || + img.closest('.text-content'); + if (!hit) return; + const url = img.currentSrc || img.src; + if (!url) return; + useUiStore().openImagePreview({ url, name: img.alt || '' }); + }); +} + export function setupShowImageObserver() { if (showImageObserver || showContainerObserver) return; setupLayoutDebugObservers(); @@ -1471,6 +1507,9 @@ function renderShowFileCard(node: Element) { node.replaceChildren(mountEl); const app = createApp(ShowFileCard, { path }); + // 独立 createApp 实例不继承主应用的插件,必须显式安装 i18n, + // 否则组件模板里的 $t 不可用(报 "j.$t is not a function") + app.use(i18n); app.mount(mountEl); showFileAppMap.set(node, app); } diff --git a/static/src/app/lifecycle.ts b/static/src/app/lifecycle.ts index 0e260caa..f21dd72d 100644 --- a/static/src/app/lifecycle.ts +++ b/static/src/app/lifecycle.ts @@ -2,7 +2,7 @@ import { useChatActionStore } from '../stores/chatActions'; import { useSandboxSetupStore } from '../stores/sandboxSetup'; import { normalizeScrollLock } from '../composables/useScrollControl'; -import { setupShowImageObserver, teardownShowImageObserver } from './bootstrap'; +import { setupShowImageObserver, teardownShowImageObserver, setupImagePreviewDelegation } from './bootstrap'; import { debugLog } from './methods/common'; export function created() { @@ -41,6 +41,7 @@ export async function mounted() { normalizeScrollLock(this); }); setupShowImageObserver(); + setupImagePreviewDelegation(); // 立即加载初始数据(并行获取状态,优先同步运行模式) const initialDataPromise = this.loadInitialData(); diff --git a/static/src/components/chat/CitationPopover.vue b/static/src/components/chat/CitationPopover.vue index ebc37060..5971d0a4 100644 --- a/static/src/components/chat/CitationPopover.vue +++ b/static/src/components/chat/CitationPopover.vue @@ -27,7 +27,12 @@
{{ single.url }}
- +
“{{ displaySnippet(single) }}”
@@ -132,6 +137,14 @@ function fileContentUrl(ann: CitationAnnotation): string { return `/api/file/content?path=${encodeURIComponent(ann.file_path || '')}`; } +/** 点击引用图片缩略图:打开全局灯箱预览 */ +function openCitationPreview(ann: CitationAnnotation) { + uiStore.openImagePreview({ + url: fileContentUrl(ann), + name: ann.file_name || '' + }); +} + function shortDomain(domain?: string) { return (domain || '').replace(/^www\./, ''); } diff --git a/static/src/components/chat/EditSummaryCard.vue b/static/src/components/chat/EditSummaryCard.vue index 453d2a00..f4a28b57 100644 --- a/static/src/components/chat/EditSummaryCard.vue +++ b/static/src/components/chat/EditSummaryCard.vue @@ -17,6 +17,7 @@ */ import { computed, onBeforeUnmount, ref, watch } from 'vue'; import { useUiStore } from '@/stores/ui'; +import CloseButton from '@/components/common/CloseButton.vue'; const props = defineProps<{ summary: any; @@ -342,15 +343,12 @@ function lineNumber(line: any): string { +{{ activeFile.added || 0 }} -{{ activeFile.removed || 0 }} - + />
@@ -82,6 +82,7 @@ import { buildShowHtmlIframeSrcdoc } from '@/utils/showHtmlSandbox'; import { openShowHtmlFullscreen } from '@/utils/showHtmlFullscreen'; import PdfPreview from './PdfPreview.vue'; import { t } from '@/locales'; +import { useUiStore } from '@/stores/ui'; // 文本类文件内容缓存,避免父组件反复重建卡片时重复 fetch 导致闪烁 const SHOW_FILE_CONTENT_CACHE = new Map(); @@ -430,7 +431,9 @@ async function copyContent() { } function openFullImage() { - if (contentUrl.value) window.open(contentUrl.value, '_blank'); + if (!contentUrl.value) return; + // 全站统一走全局灯箱预览(Android WebView 同样适用,不再新开标签页) + useUiStore().openImagePreview({ url: contentUrl.value, name: displayName.value || '' }); } function onImageError() { diff --git a/static/src/components/chat/actions/toolRenderers.ts b/static/src/components/chat/actions/toolRenderers.ts index 07a20754..b5749fc5 100644 --- a/static/src/components/chat/actions/toolRenderers.ts +++ b/static/src/components/chat/actions/toolRenderers.ts @@ -768,7 +768,9 @@ function renderViewImage(result: any, args: any): string { if (imageUrl) { const safeUrl = escapeHtml(imageUrl); - html += `
${escapeHtml(t('toolResults.media.viewImgAlt'))}
`; + // 不再用 打开新标签页,直接渲染 , + // 由聊天消息区的事件委托统一承接点击灯箱预览。 + html += `
${escapeHtml(t('toolResults.media.viewImgAlt'))}
`; } return html; diff --git a/static/src/components/chat/quickdock/FilePreviewPanel.vue b/static/src/components/chat/quickdock/FilePreviewPanel.vue index 836c3fec..1c476615 100644 --- a/static/src/components/chat/quickdock/FilePreviewPanel.vue +++ b/static/src/components/chat/quickdock/FilePreviewPanel.vue @@ -25,16 +25,7 @@ {{ fileName }} {{ dirName }} - +
{{ $t('common.loading') }}
@@ -70,6 +61,7 @@ + + + + diff --git a/static/src/components/overlay/BackgroundCommandDialog.vue b/static/src/components/overlay/BackgroundCommandDialog.vue index 5a8bb3d8..96bcde32 100644 --- a/static/src/components/overlay/BackgroundCommandDialog.vue +++ b/static/src/components/overlay/BackgroundCommandDialog.vue @@ -6,7 +6,7 @@
{{ $t('overlay.bgCommandTitle', { id: activeCommand.command_id }) }}
- +
{{ $t('overlay.generatedHint', { path: generatedPath }) }}
- + @click="$emit('close')" + />
@@ -127,6 +123,8 @@