From 49433f168109d9c23b8feaa9298916dad27a58a3 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Mon, 9 Mar 2026 15:42:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Knowledge=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化文档预览功能 - 添加 CSV 文件解析支持 - 增强知识库详情展示 - 优化样式和交互 Co-Authored-By: Claude Opus 4.6 --- web/src/views/Knowledge.vue | 224 +++++++++++++++++++++++--- web/src/views/Settings.vue | 1 + web/src/views/knowledge/knowledge.css | 160 ++++++++++++++++++ 3 files changed, 366 insertions(+), 19 deletions(-) diff --git a/web/src/views/Knowledge.vue b/web/src/views/Knowledge.vue index eae91d8..9367848 100644 --- a/web/src/views/Knowledge.vue +++ b/web/src/views/Knowledge.vue @@ -3,6 +3,10 @@ import { ref, computed, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { useModelSettings } from './settings/useModelSettings' import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge' +import VueOfficeDocx from '@vue-office/docx' +import VueOfficeExcel from '@vue-office/excel' +import Papa from 'papaparse' +import { marked } from 'marked' import './knowledge/knowledge.css' // 获取已配置的模型列表 @@ -135,7 +139,12 @@ const loadingDocuments = ref(false) const fileInput = ref(null) const uploading = ref(false) const previewUrl = ref('') // 文档预览URL (blob URL) +const previewHtml = ref('') // HTML内容预览 +const previewContentType = ref('') // content_type: url 或 html const previewDownloadUrl = ref('') // 原始下载链接 +const previewFileType = ref('') // 文件类型: pdf, docx, xlsx, csv +const previewCsvData = ref([]) // CSV数据 +const previewCsvHeaders = ref([]) // CSV表头 const loadingPreview = ref(false) const previewPage = ref(1) // 当前页码 const previewTotalPages = ref(1) // 总页数 @@ -157,6 +166,20 @@ const loadPdfWithProxy = async (doc: any): Promise => { } } +// 将URL转换为blob URL (用于Excel组件) +const loadFileAsBlob = async (url: string): Promise => { + try { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status}`) + } + return await response.blob() + } catch (error) { + console.error('Failed to load file as blob:', error) + throw error + } +} + const newKbForm = ref({ name: '', description: '', @@ -337,6 +360,11 @@ const enterKnowledge = async (kb: any) => { selectedKnowledge.value = kb selectedFile.value = null previewUrl.value = '' + previewHtml.value = '' + previewContentType.value = '' + previewFileType.value = '' + previewCsvData.value = [] + previewCsvHeaders.value = [] previewDownloadUrl.value = '' // 获取文档列表 @@ -375,13 +403,126 @@ const selectDocument = async (doc: any) => { selectedFile.value = doc.id selectedDocument.value = doc previewUrl.value = '' + previewHtml.value = '' + previewContentType.value = '' previewDownloadUrl.value = '' + previewFileType.value = '' + previewCsvData.value = [] + previewCsvHeaders.value = [] previewPage.value = 1 previewTotalPages.value = 1 - // 优先使用代理接口加载PDF + // 检测文件类型 + const fileName = doc.name || '' + const ext = fileName.split('.').pop()?.toLowerCase() || '' + console.log('File name:', fileName, 'Ext:', ext) + if (ext === 'pdf') { + previewFileType.value = 'pdf' + } else if (ext === 'docx' || ext === 'doc') { + previewFileType.value = 'docx' + } else if (ext === 'xlsx' || ext === 'xls') { + previewFileType.value = 'xlsx' + } else if (ext === 'csv') { + previewFileType.value = 'csv' + } else if (ext === 'txt' || ext === 'md') { + previewFileType.value = 'text' + } + console.log('Preview file type:', previewFileType.value) + + // 纯文本文件 (txt, md) 直接读取内容显示 + if (previewFileType.value === 'text') { + // 优先使用 file_url(上传时后端返回的) + let url = doc.file_url || doc.fileUrl || doc.url || doc.FileURL || '' + // 如果没有 file_url,尝试用 file_key 通过代理 + if (!url && doc.file_key && selectedKnowledge.value) { + url = await loadPdfWithProxy(doc) + } + + console.log('Text file URL:', url) + if (url) { + try { + const response = await fetch(url) + console.log('Response status:', response.status) + const arrayBuffer = await response.arrayBuffer() + const decoder = new TextDecoder('utf-8') + const content = decoder.decode(arrayBuffer) + console.log('Content length:', content.length, 'ext:', ext) + + // .md 文件用 marked 渲染 + if (ext === 'md') { + // marked.parse 返回 Promise,需要 await + const html = await marked.parse(content) + previewHtml.value = html as string + } else { + // .txt 文件直接显示 + previewHtml.value = '
' + content + '
' + } + console.log('PreviewHtml set, length:', previewHtml.value.length) + return + } catch (error) { + console.error('Failed to load text file:', error) + } + } else { + console.log('No URL for text file, file_key:', doc.file_key, 'file_url:', doc.file_url) + } + } + + // CSV需要特殊处理:用PapaParse解析 + if (previewFileType.value === 'csv' && (doc.file_key || doc.file_url)) { + const url = doc.file_key && selectedKnowledge.value + ? await loadPdfWithProxy(doc) + : doc.file_url || doc.fileUrl || doc.url || '' + if (url) { + try { + const response = await fetch(url) + const arrayBuffer = await response.arrayBuffer() + + // 尝试多种编码解码 + let csvText = '' + const encodings = ['utf-8', 'gbk', 'gb2312', 'big5'] + for (const encoding of encodings) { + try { + const decoder = new TextDecoder(encoding) + csvText = decoder.decode(arrayBuffer) + // 检查是否成功解码(如果没有乱码字符) + if (!csvText.includes('\uFFFD')) { + console.log(`CSV decoded with: ${encoding}`) + break + } + } catch (e) { + continue + } + } + + const result = Papa.parse(csvText, { header: false, skipEmptyLines: true }) + if (result.data && result.data.length > 0) { + previewCsvHeaders.value = result.data[0] as string[] + previewCsvData.value = result.data.slice(1) as any[][] + } + return + } catch (error) { + console.error('Failed to parse CSV:', error) + } + } + } + + // 使用代理接口加载文件 if (doc.file_key && selectedKnowledge.value) { - previewUrl.value = await loadPdfWithProxy(doc) + const proxyUrl = await loadPdfWithProxy(doc) + + // Excel/Word需要blob URL + if ((previewFileType.value === 'xlsx' || previewFileType.value === 'docx') && proxyUrl) { + try { + const blob = await loadFileAsBlob(proxyUrl) + const blobUrl = URL.createObjectURL(blob) + previewUrl.value = blobUrl + return + } catch (error) { + console.error('Failed to convert to blob:', error) + } + } + + previewUrl.value = proxyUrl if (previewUrl.value) { return } @@ -397,11 +538,19 @@ const selectDocument = async (doc: any) => { // 如果没有file_url,调用预览API获取 if (selectedKnowledge.value) { loadingPreview.value = true + previewUrl.value = '' + previewHtml.value = '' + previewContentType.value = '' try { const { getDocumentPreview } = await import('./knowledge/useKnowledge') const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id) if (result.success && result.data?.content) { - previewUrl.value = result.data.content + previewContentType.value = result.data.content_type || 'url' + if (previewContentType.value === 'html') { + previewHtml.value = result.data.content + } else { + previewUrl.value = result.data.content + } previewTotalPages.value = result.data.total_pages || 1 previewPage.value = result.data.current_page || 1 } @@ -416,6 +565,8 @@ const selectDocument = async (doc: any) => { // 翻页 const changePreviewPage = async (page: number) => { if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return + // HTML内容不支持翻页 + if (previewContentType.value === 'html') return loadingPreview.value = true previewPage.value = page @@ -423,7 +574,12 @@ const changePreviewPage = async (page: number) => { const { getDocumentPreview } = await import('./knowledge/useKnowledge') const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page) if (result.success && result.data?.content) { - previewUrl.value = result.data.content + previewContentType.value = result.data.content_type || 'url' + if (previewContentType.value === 'html') { + previewHtml.value = result.data.content + } else { + previewUrl.value = result.data.content + } previewTotalPages.value = result.data.total_pages || 1 previewPage.value = result.data.current_page || page } @@ -459,16 +615,8 @@ const handleFileSelect = async (event: Event) => { // 获取刚上传的文档 const uploadedDoc = knowledgeDocuments.value.find(d => d.id === result.id) if (uploadedDoc) { - // 选中新上传的文档 - selectedFile.value = result.id - selectedDocument.value = uploadedDoc - - // 使用代理接口加载PDF - if (uploadedDoc.file_key) { - previewUrl.value = await loadPdfWithProxy(uploadedDoc) - } else if (uploadedDoc.file_url) { - previewUrl.value = uploadedDoc.file_url - } + // 选中新上传的文档,调用 selectDocument 处理预览 + await selectDocument(uploadedDoc) } } else { ElMessage.error(result.message || 'Failed to upload file') @@ -497,7 +645,12 @@ const deleteDocument = async (docId: string) => { selectedFile.value = null selectedDocument.value = null previewUrl.value = '' - previewDownloadUrl.value = '' + previewHtml.value = '' + previewContentType.value = '' + previewFileType.value = '' + previewCsvData.value = [] + previewCsvHeaders.value = [] + previewDownloadUrl.value = '' } // 刷新文档列表 await changeFileFilter(fileFilter.value) @@ -873,7 +1026,7 @@ const deleteDocument = async (docId: string) => { type="file" ref="fileInput" style="display: none" - accept=".pdf,.doc,.docx,.txt,.md" + accept=".pdf,.doc,.docx,.docx,.txt,.md,.csv,.xlsx,.xls,.pptx,.ppt" @change="handleFileSelect" />