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