feat: 增强 Knowledge 页面功能
- 优化文档预览功能 - 添加 CSV 文件解析支持 - 增强知识库详情展示 - 优化样式和交互 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<HTMLInputElement | null>(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<any[]>([]) // CSV数据
|
||||
const previewCsvHeaders = ref<string[]>([]) // CSV表头
|
||||
const loadingPreview = ref(false)
|
||||
const previewPage = ref(1) // 当前页码
|
||||
const previewTotalPages = ref(1) // 总页数
|
||||
@@ -157,6 +166,20 @@ const loadPdfWithProxy = async (doc: any): Promise<string> => {
|
||||
}
|
||||
}
|
||||
|
||||
// 将URL转换为blob URL (用于Excel组件)
|
||||
const loadFileAsBlob = async (url: string): Promise<Blob> => {
|
||||
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 = '<pre style="white-space: pre-wrap; word-wrap: break-word; color: #e8eaed;">' + content + '</pre>'
|
||||
}
|
||||
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"
|
||||
/>
|
||||
<button class="btn-primary" @click="triggerFileUpload">
|
||||
@@ -987,7 +1140,40 @@ const deleteDocument = async (docId: string) => {
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
<span>Loading preview...</span>
|
||||
</div>
|
||||
<!-- 有blob预览URL时显示PDF (使用iframe) -->
|
||||
<!-- Word文件预览 -->
|
||||
<VueOfficeDocx
|
||||
v-else-if="previewUrl && previewFileType === 'docx'"
|
||||
:src="previewUrl"
|
||||
class="office-embed"
|
||||
@rendered="console.log('Docx rendered')"
|
||||
@error="console.error('Docx error', $event)"
|
||||
/>
|
||||
<!-- Excel文件预览 -->
|
||||
<VueOfficeExcel
|
||||
v-else-if="previewUrl && previewFileType === 'xlsx'"
|
||||
:src="previewUrl"
|
||||
class="office-embed"
|
||||
@rendered="console.log('Excel rendered')"
|
||||
@error="console.error('Excel error', $event)"
|
||||
/>
|
||||
<!-- CSV文件预览 -->
|
||||
<div v-else-if="previewFileType === 'csv' && previewCsvData.length > 0" class="csv-preview">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(header, index) in previewCsvHeaders" :key="index">{{ header }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in previewCsvData" :key="rowIndex">
|
||||
<td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 文本文件预览 (txt, md) -->
|
||||
<div v-else-if="previewHtml" class="text-preview" v-html="previewHtml"></div>
|
||||
<!-- PDF/其他文件预览 (使用iframe) -->
|
||||
<iframe
|
||||
v-else-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
@@ -996,9 +1182,9 @@ const deleteDocument = async (docId: string) => {
|
||||
<!-- 无预览但有下载链接时显示下载按钮 -->
|
||||
<div v-else-if="previewDownloadUrl" class="preview-no-file">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span>Cannot preview PDF directly</span>
|
||||
<span>Cannot preview file directly</span>
|
||||
<a :href="previewDownloadUrl" target="_blank" class="download-link">
|
||||
<i class="fa-solid fa-download"></i> Download PDF
|
||||
<i class="fa-solid fa-download"></i> Download File
|
||||
</a>
|
||||
</div>
|
||||
<!-- 无预览也无下载链接时显示提示 -->
|
||||
|
||||
@@ -241,6 +241,7 @@ const showChangePassword = () => {
|
||||
description="Configure your model settings"
|
||||
icon="fa-solid fa-brain"
|
||||
icon-class="bg-gradient-to-br from-primary-orange to-red-500"
|
||||
class="add-model-dialog"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
|
||||
@@ -943,6 +943,166 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* HTML内容预览 */
|
||||
.html-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
background-color: #1e1e24;
|
||||
color: #e8eaed;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.html-preview table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.html-preview table th,
|
||||
.html-preview table td {
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.html-preview table th {
|
||||
background-color: #2a2a3a;
|
||||
}
|
||||
|
||||
.html-preview h1, .html-preview h2, .html-preview h3 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.html-preview pre {
|
||||
background-color: #2a2a3a;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.html-preview code {
|
||||
background-color: #2a2a3a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Office 组件预览 */
|
||||
.office-embed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 修复 vue-office 表格黑色背景问题 */
|
||||
.office-embed :deep(table) {
|
||||
background-color: #ffffff !important;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.office-embed :deep(td),
|
||||
.office-embed :deep(th) {
|
||||
background-color: #ffffff !important;
|
||||
color: #333333 !important;
|
||||
border: 1px solid #dddddd !important;
|
||||
}
|
||||
|
||||
/* CSV 表格预览 */
|
||||
.csv-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #ffffff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.csv-preview table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.csv-preview th,
|
||||
.csv-preview td {
|
||||
border: 1px solid #dddddd;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.csv-preview th {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.csv-preview tr:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 文本文件预览 */
|
||||
.text-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
background-color: #1e1e24;
|
||||
color: #e8eaed;
|
||||
}
|
||||
|
||||
.text-preview pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
color: #e8eaed;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Markdown 渲染样式 */
|
||||
.text-preview h1, .text-preview h2, .text-preview h3,
|
||||
.text-preview h4, .text-preview h5, .text-preview h6 {
|
||||
color: #ffffff;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.text-preview code {
|
||||
background-color: #2a2a3a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.text-preview pre {
|
||||
background-color: #2a2a3a;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.text-preview ul, .text-preview ol {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.text-preview li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.text-preview a {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.text-preview blockquote {
|
||||
border-left: 4px solid #f97316;
|
||||
padding-left: 16px;
|
||||
margin-left: 0;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.preview-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user