fix: 调整工作台文件预览全屏交互

This commit is contained in:
Wenjie Zhang 2026-03-30 16:26:46 +08:00
parent 94e16802d1
commit fa5a2fa1ae
2 changed files with 185 additions and 6 deletions

View File

@ -53,6 +53,7 @@
### 修复
- 调整前端工作台文件预览交互:恢复默认侧边/弹窗预览,并新增显式“全屏预览”入口;全屏模式下由预览内容直接覆盖整页,仅保留右上角悬浮关闭按钮
- 兼容旧版已安装的内置 `reporter` 技能记录:`update_builtin_skill` 现在会识别由 `system``builtin-system` 管理的历史记录,避免更新时误报“技能 `reporter` 不是内置 skill”
- 调整沙盒 user-data 目录隔离策略:`workspace` 改为共享目录 `saves/threads/shared/workspace``uploads/outputs` 继续保持 thread 级隔离;同时更新 thread artifact 权限校验、viewer 文件系统列举逻辑,以及对应的 router/E2E 测试
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试

View File

@ -136,6 +136,9 @@
<button class="modal-action-btn" @click="downloadFile(currentFile)" title="下载">
<Download :size="18" />
</button>
<button class="modal-action-btn" @click="openFullscreenPreview" title="全屏预览">
<Maximize2 :size="18" />
</button>
<button class="modal-action-btn" @click="closePreview" title="关闭预览">
<X :size="18" />
</button>
@ -207,7 +210,6 @@
</div>
</div>
<!-- 文件内容 Modal -->
<a-modal
v-model:open="modalVisible"
width="800px"
@ -246,13 +248,21 @@
</button>
</div>
<button
v-if="currentFile"
class="modal-action-btn"
@click="downloadFile(currentFile)"
v-if="currentFile"
title="下载"
>
<Download :size="18" />
</button>
<button
v-if="currentFile"
class="modal-action-btn"
@click="openFullscreenPreview"
title="全屏预览"
>
<Maximize2 :size="18" />
</button>
<button class="modal-action-btn" @click="closePreview" title="关闭">
<X :size="18" />
</button>
@ -290,9 +300,7 @@
</div>
</template>
<template v-else>
<pre v-if="Array.isArray(currentFile?.content)">{{
formatContent(currentFile.content)
}}</pre>
<pre v-if="Array.isArray(currentFile?.content)">{{ formatContent(currentFile.content) }}</pre>
<pre
v-else-if="isCodePreview && typeof currentFile?.content === 'string'"
:class="['file-content-pre', 'code-highlight', codeThemeClass]"
@ -304,6 +312,88 @@
</template>
</div>
</a-modal>
<Teleport to="body">
<div v-if="fullscreenPreviewVisible && currentFile" class="fullscreen-preview-overlay">
<div class="fullscreen-preview-actions">
<div v-if="isHtmlFile" class="preview-mode-switch fullscreen-preview-switch">
<button
class="preview-mode-btn"
:class="{ active: htmlPreviewMode === 'render' }"
@click="htmlPreviewMode = 'render'"
title="预览"
>
<Globe :size="16" />
</button>
<button
class="preview-mode-btn"
:class="{ active: htmlPreviewMode === 'source' }"
@click="htmlPreviewMode = 'source'"
title="源码"
>
<Code2 :size="16" />
</button>
</div>
<button
class="modal-action-btn fullscreen-action-btn"
@click="closeFullscreenPreview"
title="关闭"
>
<X :size="18" />
</button>
</div>
<div class="fullscreen-preview-content">
<div class="file-content fullscreen-file-content">
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
<div class="image-preview-wrapper fullscreen-image-preview-wrapper">
<img :src="currentFile.previewUrl" :alt="currentFilePath" class="image-preview" />
</div>
</template>
<template v-else-if="currentFile?.previewType === 'pdf' && currentFile?.previewUrl">
<iframe
:src="currentFile.previewUrl"
class="pdf-preview fullscreen-embed-preview"
:title="currentFilePath"
/>
</template>
<template v-else-if="isHtmlFile && htmlPreviewMode === 'render'">
<iframe
class="html-preview fullscreen-embed-preview"
:srcdoc="formatContent(currentFile?.content)"
:title="currentFilePath"
sandbox=""
/>
</template>
<template v-else-if="isMarkdown">
<MdPreview
class="flat-md-preview"
:modelValue="formatContent(currentFile?.content)"
:theme="theme"
previewTheme="github"
/>
</template>
<template v-else-if="currentFile?.supported === false">
<div class="unsupported-preview fullscreen-unsupported-preview">
{{ currentFile?.message || '当前文件暂不支持预览,请下载后查看' }}
</div>
</template>
<template v-else>
<pre v-if="Array.isArray(currentFile?.content)">{{
formatContent(currentFile.content)
}}</pre>
<pre
v-else-if="isCodePreview && typeof currentFile?.content === 'string'"
:class="['file-content-pre', 'code-highlight', codeThemeClass]"
><code class="hljs" v-html="highlightedCodeContent"></code></pre>
<pre v-else-if="typeof currentFile?.content === 'string'" class="file-content-pre">{{
currentFile.content
}}</pre>
<pre v-else>{{ JSON.stringify(currentFile, null, 2) }}</pre>
</template>
</div>
</div>
</div>
</Teleport>
</div>
</template>
@ -316,6 +406,7 @@ import {
Download,
FolderCode,
Globe,
Maximize2,
RefreshCw,
X
} from 'lucide-vue-next'
@ -380,6 +471,7 @@ const INLINE_PREVIEW_MIN_WIDTH = 920
const panelRef = ref(null)
const activeTab = ref('files')
const modalVisible = ref(false)
const fullscreenPreviewVisible = ref(false)
const currentFile = ref(null)
const currentFilePath = ref('')
const htmlPreviewMode = ref('render')
@ -717,6 +809,7 @@ const onFileSelect = async (nextSelectedKeys, { node }) => {
}
const closePreview = () => {
fullscreenPreviewVisible.value = false
revokeCurrentPreviewUrl()
modalVisible.value = false
currentFile.value = null
@ -725,6 +818,15 @@ const closePreview = () => {
selectedKeys.value = []
}
const openFullscreenPreview = () => {
if (!currentFile.value) return
fullscreenPreviewVisible.value = true
}
const closeFullscreenPreview = () => {
fullscreenPreviewVisible.value = false
}
const downloadFile = async (fileItem) => {
if (!props.threadId || !fileItem?.path) return
@ -847,6 +949,7 @@ onUnmounted(() => {
window.removeEventListener('pointercancel', stopResize)
document.body.style.cursor = ''
document.body.style.userSelect = ''
document.body.style.overflow = ''
revokeCurrentPreviewUrl()
})
@ -869,6 +972,14 @@ watch(useInlinePreview, (isInline) => {
modalVisible.value = !isInline
})
watch([modalVisible, fullscreenPreviewVisible], ([modalOpen, fullscreenOpen]) => {
document.body.style.overflow = fullscreenOpen ? 'hidden' : ''
if (fullscreenOpen && modalOpen) {
modalVisible.value = false
}
})
</script>
<style scoped lang="less">
@ -1305,6 +1416,66 @@ watch(useInlinePreview, (isInline) => {
}
}
.fullscreen-preview-overlay {
position: fixed;
inset: 0;
z-index: 1200;
background: var(--gray-0);
}
.fullscreen-preview-actions {
position: fixed;
top: 16px;
right: 16px;
z-index: 2;
display: flex;
align-items: center;
gap: 8px;
}
.fullscreen-preview-switch {
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
}
.fullscreen-action-btn {
width: 40px;
height: 40px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid var(--gray-200);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
backdrop-filter: blur(10px);
}
.fullscreen-preview-content {
position: absolute;
inset: 0;
min-height: 0;
}
.fullscreen-file-content {
height: 100vh;
max-height: none;
min-height: 100vh;
padding: 24px;
border-radius: 0;
}
.fullscreen-image-preview-wrapper {
min-height: calc(100vh - 48px);
align-items: center;
}
.fullscreen-embed-preview {
min-height: calc(100vh - 48px);
height: calc(100vh - 48px);
border-radius: 12px;
}
.fullscreen-unsupported-preview {
min-height: calc(100vh - 48px);
}
.file-content-pre.code-highlight {
border-radius: 8px;
background: var(--gray-25);
@ -1321,6 +1492,10 @@ watch(useInlinePreview, (isInline) => {
min-height: calc(80vh - 40px);
}
.fullscreen-file-content .file-content-pre.code-highlight code {
min-height: calc(100vh - 48px);
}
.image-preview-wrapper {
display: flex;
justify-content: center;
@ -1335,6 +1510,10 @@ watch(useInlinePreview, (isInline) => {
border-radius: 6px;
}
.fullscreen-file-content .image-preview {
max-height: calc(100vh - 48px);
}
.pdf-preview {
width: 100%;
min-height: calc(80vh - 40px);
@ -1443,7 +1622,6 @@ watch(useInlinePreview, (isInline) => {
white-space: nowrap;
}
/* Modal 样式优化 - shadcn 风格 */
:deep(.ant-modal) {
z-index: 1050;
}