+
getFileIconColor(file.value?.filename))
const downloadingOriginal = ref(false)
const downloadingMarkdown = ref(false)
+const sourcePreviewLoading = ref(false)
+const sourcePreviewUrl = ref('')
// 主题设置
const theme = computed(() => (themeStore.isDark ? 'dark' : 'light'))
// 视图模式
const viewMode = ref('markdown')
-const hasIndexed = computed(() => ['done', 'indexed'].includes(file.value?.status))
const hasContent = computed(
() => (file.value?.lines && file.value?.lines.length > 0) || file.value?.content
)
+const sourcePreviewType = computed(() => getPreviewTypeByPath(file.value?.filename || ''))
+const hasSourcePreview = computed(() => ['image', 'pdf'].includes(sourcePreviewType.value))
// 是否有实际的分块数据
const hasChunks = computed(() => mappedChunks.value && mappedChunks.value.length > 0)
const viewModeOptions = computed(() => {
- const options = [{ label: 'Markdown', value: 'markdown' }]
+ const options = []
+ if (hasSourcePreview.value) {
+ options.push({ label: '源文件', value: 'source' })
+ }
+ options.push({ label: 'Markdown', value: 'markdown' })
// 只有当有实际的分块数据时才显示 Chunks 选项
if (hasChunks.value) {
options.push({ label: 'Chunks', value: 'chunks' })
@@ -154,12 +183,36 @@ const viewModeOptions = computed(() => {
})
// 监听文件变化,如果没有 chunks 则重置为 markdown
-watch(file, (newFile) => {
- if (newFile && !hasChunks.value) {
- viewMode.value = 'markdown'
+watch(file, (newFile, oldFile) => {
+ if (newFile?.file_id !== oldFile?.file_id) {
+ revokeSourcePreviewUrl()
+ }
+
+ if (!newFile) {
+ revokeSourcePreviewUrl()
+ return
+ }
+
+ if (!hasChunks.value) {
+ viewMode.value = hasSourcePreview.value ? 'source' : 'markdown'
}
})
+watch(
+ [visible, file],
+ async ([open, currentFile]) => {
+ if (!open || !currentFile || !hasSourcePreview.value) {
+ if (!open || !hasSourcePreview.value) {
+ revokeSourcePreviewUrl()
+ }
+ return
+ }
+
+ await loadSourcePreview()
+ },
+ { immediate: true }
+)
+
// 统计信息
const mergeResult = computed(() => mergeChunks(file.value?.lines || []))
const mappedChunks = computed(() => mergeResult.value.chunks)
@@ -180,11 +233,37 @@ function formatTextLength(length) {
const afterOpenChange = (open) => {
if (!open) {
+ revokeSourcePreviewUrl()
store.selectedFile = null
viewMode.value = 'markdown'
}
}
+const revokeSourcePreviewUrl = () => {
+ if (sourcePreviewUrl.value) {
+ window.URL.revokeObjectURL(sourcePreviewUrl.value)
+ sourcePreviewUrl.value = ''
+ }
+}
+
+const loadSourcePreview = async () => {
+ if (!file.value?.file_id || !store.databaseId || !hasSourcePreview.value) return
+ if (sourcePreviewUrl.value) return
+
+ sourcePreviewLoading.value = true
+ try {
+ const response = await documentApi.downloadDocument(store.databaseId, file.value.file_id)
+ const blob = await response.blob()
+ revokeSourcePreviewUrl()
+ sourcePreviewUrl.value = window.URL.createObjectURL(blob)
+ } catch (error) {
+ console.error('加载源文件预览失败:', error)
+ message.error(error.message || '加载源文件预览失败')
+ } finally {
+ sourcePreviewLoading.value = false
+ }
+}
+
// 下载菜单点击处理
const handleDownloadMenuClick = ({ key }) => {
if (key === 'original') {
@@ -314,7 +393,7 @@ const handleDownloadMarkdown = () => {
.chunks-panel {
flex: 1;
overflow-y: auto;
- padding: 16px 0;
+ padding: 0;
min-height: 0;
}
@@ -322,6 +401,30 @@ const handleDownloadMarkdown = () => {
min-height: 100%;
}
+.source-preview-wrapper {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.source-image {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: 8px;
+}
+
+.source-pdf {
+ width: 100%;
+ max-height: 100%;
+ height: calc(100% - 6px);
+ border: none;
+ border-radius: 8px;
+ background: var(--gray-25);
+}
+
.loading-container {
display: flex;
justify-content: center;
diff --git a/web/src/utils/file_preview.js b/web/src/utils/file_preview.js
index 1de7d080..aca4ceb6 100644
--- a/web/src/utils/file_preview.js
+++ b/web/src/utils/file_preview.js
@@ -1,4 +1,6 @@
const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown', '.mdx'])
+const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'])
+const PDF_EXTENSIONS = new Set(['.pdf'])
export const getPreviewFileExtension = (path) => {
const normalizedPath = String(path || '')
@@ -16,3 +18,11 @@ export const isMarkdownPreview = (path, previewType) => {
if (previewType === 'markdown') return true
return MARKDOWN_EXTENSIONS.has(getPreviewFileExtension(path))
}
+
+export const getPreviewTypeByPath = (path) => {
+ const extension = getPreviewFileExtension(path)
+ if (IMAGE_EXTENSIONS.has(extension)) return 'image'
+ if (PDF_EXTENSIONS.has(extension)) return 'pdf'
+ if (MARKDOWN_EXTENSIONS.has(extension)) return 'markdown'
+ return 'text'
+}