feat(web): 统一交付物和文件树的预览逻辑

This commit is contained in:
Wenjie Zhang 2026-03-31 19:42:35 +08:00
parent 0d1e26cf7b
commit 824270a035
4 changed files with 63 additions and 40 deletions

View File

@ -59,7 +59,7 @@
- 优化 Agent 输入框 mention 行为:在保留附件 mention 的同时,将共享 `workspace` 文件纳入候选范围;并将 `@` 空查询时的候选列表改为空,仅在继续输入后再执行筛选,避免工作区文件过多时直接铺满下拉面板
- 为前端工作台文件树补齐文件删除能力:`/api/viewer/filesystem/file` 新增删除接口,`AgentPanel` 文件节点新增删除按钮与确认交互,删除后会同步刷新树与预览状态
- 调整前端工作台文件预览交互:恢复默认侧边/弹窗预览,并新增显式“全屏预览”入口;全屏模式下由预览内容直接覆盖整页,仅保留右上角悬浮关闭按钮;
- 调整前端工作台文件预览交互:恢复默认侧边/弹窗预览,并新增显式“全屏预览”入口;全屏模式下由预览内容直接覆盖整页,仅保留右上角悬浮关闭按钮;同时修复 HTML 文件首次在弹窗中预览偶现白屏的问题,改为在内容更新后强制重建 `iframe`
- 统一 Agent Panel 文件预览与消息区交付物预览组件:两处改为复用同一套 `AgentFilePreview` 预览实现,并为交付物预览补齐与工作台一致的“全屏预览”入口
- 兼容旧版已安装的内置 `reporter` 技能记录:`update_builtin_skill` 现在会识别由 `system``builtin-system` 管理的历史记录,避免更新时误报“技能 `reporter` 不是内置 skill”
- 调整沙盒 user-data 目录隔离策略:`workspace` 改为共享目录 `saves/threads/shared/workspace``uploads/outputs` 继续保持 thread 级隔离;同时更新 thread artifact 权限校验、viewer 文件系统列举逻辑,以及对应的 router/E2E 测试

View File

@ -51,6 +51,7 @@
:bodyStyle="{ maxHeight: '90vh', overflow: 'auto' }"
:footer="null"
:closable="false"
wrapClassName="agent-file-preview-modal"
@cancel="closePreview"
>
<AgentFilePreview

View File

@ -60,10 +60,11 @@
</template>
<template v-else-if="isHtmlFile && htmlPreviewMode === 'render'">
<iframe
:key="`embedded-${htmlPreviewRenderKey}`"
class="html-preview"
:srcdoc="formatContent(file?.content)"
:title="filePath"
sandbox=""
sandbox="allow-scripts"
/>
</template>
<template v-else-if="isMarkdown">
@ -143,10 +144,11 @@
</template>
<template v-else-if="isHtmlFile && htmlPreviewMode === 'render'">
<iframe
:key="`fullscreen-${htmlPreviewRenderKey}`"
class="html-preview fullscreen-embed-preview"
:srcdoc="formatContent(file?.content)"
:title="filePath"
sandbox=""
sandbox="allow-scripts"
/>
</template>
<template v-else-if="isMarkdown">
@ -237,6 +239,7 @@ const themeStore = useThemeStore()
const theme = computed(() => (themeStore.isDark ? 'dark' : 'light'))
const htmlPreviewMode = ref('render')
const fullscreenPreviewVisible = ref(false)
const htmlPreviewRenderKey = ref(0)
const isMarkdown = computed(() => isMarkdownPreview(props.filePath, props.file?.previewType))
const isHtmlFile = computed(
@ -302,6 +305,15 @@ watch(
}
)
watch(
[() => props.filePath, () => props.file?.previewType, () => props.file?.content],
() => {
if (isHtmlFile.value) {
htmlPreviewRenderKey.value += 1
}
}
)
watch(fullscreenPreviewVisible, (visible) => {
document.body.style.overflow = visible ? 'hidden' : ''
})
@ -314,6 +326,8 @@ onUnmounted(() => {
<style scoped lang="less">
.agent-file-preview {
min-width: 0;
border-radius: 8px;
overflow: hidden;
}
.agent-file-preview.is-full-height {
@ -396,7 +410,7 @@ onUnmounted(() => {
min-height: 300px;
max-height: 80vh;
overflow-y: auto;
border-radius: 6px;
border-radius: 0px;
&::-webkit-scrollbar {
width: 8px;
@ -431,12 +445,12 @@ onUnmounted(() => {
word-wrap: break-word;
color: var(--gray-1000);
background: transparent;
padding: 12px;
}
.file-content-pre.code-highlight {
border-radius: 8px;
background: var(--gray-25);
border: 1px solid var(--gray-150);
white-space: pre;
overflow-x: auto;
}
@ -474,9 +488,9 @@ onUnmounted(() => {
.html-preview {
width: 100%;
min-height: calc(80vh - 40px);
border: 1px solid var(--gray-150);
border-radius: 6px;
background: #fff;
border: none;
border-radius: 0px;
background: #fff; // HTML
}
.unsupported-preview {
@ -516,7 +530,7 @@ onUnmounted(() => {
width: 40px;
height: 40px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
background: var(--color-trans-light);
border: 1px solid var(--gray-200);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
backdrop-filter: blur(10px);
@ -532,7 +546,7 @@ onUnmounted(() => {
height: 100vh;
max-height: none;
min-height: 100vh;
padding: 24px;
padding: 0px;
border-radius: 0;
}
@ -542,9 +556,8 @@ onUnmounted(() => {
}
.fullscreen-embed-preview {
min-height: calc(100vh - 48px);
height: calc(100vh - 48px);
border-radius: 12px;
height: 100vh;
border-radius: 0px;
}
.fullscreen-unsupported-preview {

View File

@ -144,6 +144,7 @@
:bodyStyle="{ maxHeight: '90vh', overflow: 'auto' }"
:footer="null"
:closable="false"
wrapClassName="agent-file-preview-modal"
@cancel="closePreview"
>
<AgentFilePreview
@ -1110,33 +1111,6 @@ watch(useInlinePreview, (isInline) => {
}
}
:deep(.ant-modal) {
z-index: 1050;
}
:deep(.ant-modal-content) {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--gray-200);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
:deep(.ant-modal-header) {
background: var(--main-5);
border-bottom: 1px solid var(--gray-200);
padding: 16px 20px;
}
:deep(.ant-modal-title) {
font-weight: 600;
color: var(--gray-1000);
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
:deep(.ant-modal-body) {
padding: 0;
}
/* File Tree Styles - VS Code Style Refined */
.file-tree-container {
padding: 4px;
@ -1199,6 +1173,10 @@ watch(useInlinePreview, (isInline) => {
color: var(--main-600);
}
.tree-delete-btn:hover:not(:disabled) {
color: var(--error-600, #dc2626);
}
/* 附件列表专用样式 */
.attachment-tree :deep(.ant-tree-node-content-wrapper) {
border: 1px solid var(--gray-200);
@ -1216,3 +1194,34 @@ watch(useInlinePreview, (isInline) => {
}
}
</style>
<style lang="less">
.agent-file-preview-modal {
.ant-modal {
z-index: 1050;
.ant-modal-content {
border-radius: 8px;
padding: 0;
overflow: hidden;
border: 1px solid var(--gray-200);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
:deep(.ant-modal-header) {
background: var(--main-5);
border-bottom: 1px solid var(--gray-200);
padding: 16px 20px;
}
:deep(.ant-modal-title) {
font-weight: 600;
color: var(--gray-1000);
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
:deep(.ant-modal-body) {
padding: 0;
}
}
}
</style>