From 0b1ea33b41e4b42b27a8f4f503067979d0f24f13 Mon Sep 17 00:00:00 2001 From: yujiangping Date: Fri, 20 Mar 2026 12:13:04 +0800 Subject: [PATCH] fix:office view --- web/src/api/knowledgeBase.ts | 4 + web/src/components/DocumentPreview/index.tsx | 83 ++++++++++++++++--- .../[knowledgeBaseId]/DocumentDetails.tsx | 4 +- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/web/src/api/knowledgeBase.ts b/web/src/api/knowledgeBase.ts index 60ed2403..63ec80ae 100644 --- a/web/src/api/knowledgeBase.ts +++ b/web/src/api/knowledgeBase.ts @@ -52,6 +52,10 @@ export const getKnowledgeBaseTypeList = async (): Promise => { // 如果不是数组,返回空数组 return []; }; +// 获取文件地址 +export const getFileUrl = (fileId: string) => { + return `${apiPrefix}/files/${fileId}`; +}; // 知识库文档解析类型 export const getKnowledgeBaseDocumentParseTypeList = async () => { const response = await request.get(`${apiPrefix}/knowledges/parsertype`); diff --git a/web/src/components/DocumentPreview/index.tsx b/web/src/components/DocumentPreview/index.tsx index 247f713e..f659c53e 100644 --- a/web/src/components/DocumentPreview/index.tsx +++ b/web/src/components/DocumentPreview/index.tsx @@ -4,7 +4,7 @@ * @Author: yujiangping * @Date: 2026-03-16 19:01:12 * @LastEditors: yujiangping - * @LastEditTime: 2026-03-18 18:35:53 + * @LastEditTime: 2026-03-20 12:12:20 */ import { useState, useEffect, useRef, useCallback, type FC } from 'react'; import { Spin, Alert, Button, Table, InputNumber, Image } from 'antd'; @@ -309,23 +309,64 @@ const DocumentPreview: FC = ({ } }; + const [csvTruncated, setCsvTruncated] = useState(false); + const isCsvFile = () => getFileExtension() === '.csv'; + // CSV 预览大小限制:1MB + const CSV_PREVIEW_SIZE = 1 * 1024 * 1024; + // 最大预览行数 + const MAX_PREVIEW_ROWS = 500; + + const fetchFileBufferWithLimit = async (url: string, maxBytes?: number): Promise => { + const requestUrl = getRequestUrl(url); + const headers: Record = { + 'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`, + }; + if (maxBytes) { + headers['Range'] = `bytes=0-${maxBytes - 1}`; + } + const response = await fetch(requestUrl, { + credentials: 'include', + headers, + }); + if (!response.ok && response.status !== 206) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.arrayBuffer(); + }; + const loadExcelFile = async () => { setLoading(true); setError(false); setErrorMessage(''); + setCsvTruncated(false); try { - const arrayBuffer = await fetchFileBuffer(fileUrl); - - // CSV 文件需要处理编码问题(可能是 GBK/GB2312) + // CSV 文件需要处理编码问题(可能是 GBK/GB2312),且大文件只取前 1MB if (isCsvFile()) { + let arrayBuffer: ArrayBuffer; + let truncated = false; + try { + // 先尝试 Range 请求只取前 1MB + arrayBuffer = await fetchFileBufferWithLimit(fileUrl, CSV_PREVIEW_SIZE); + // 如果返回的数据刚好等于限制大小,说明可能被截断了 + if (arrayBuffer.byteLength >= CSV_PREVIEW_SIZE) { + truncated = true; + } + } catch { + // Range 请求不支持时,全量获取后截断 + const fullBuffer = await fetchFileBuffer(fileUrl); + if (fullBuffer.byteLength > CSV_PREVIEW_SIZE) { + arrayBuffer = fullBuffer.slice(0, CSV_PREVIEW_SIZE); + truncated = true; + } else { + arrayBuffer = fullBuffer; + } + } + let csvText: string; - // 先尝试 UTF-8 解码 const utf8Text = new TextDecoder('utf-8').decode(arrayBuffer); - // 检测是否有乱码特征(常见的 GBK 被错误解析为 UTF-8 的替换字符) if (utf8Text.includes('\uFFFD') || /[\x80-\xff]/.test(utf8Text.slice(0, 200))) { - // 尝试 GBK 解码 try { csvText = new TextDecoder('gbk').decode(arrayBuffer); } catch { @@ -334,19 +375,35 @@ const DocumentPreview: FC = ({ } else { csvText = utf8Text; } + + // 如果被截断,去掉最后一行不完整的数据 + if (truncated) { + const lastNewline = csvText.lastIndexOf('\n'); + if (lastNewline > 0) { + csvText = csvText.substring(0, lastNewline); + } + } + const workbook = XLSX.read(csvText, { type: 'string' }); const sheets = workbook.SheetNames.map(sheetName => { const worksheet = workbook.Sheets[sheetName]; - const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; + let data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; + // 限制最大行数 + if (data.length > MAX_PREVIEW_ROWS + 1) { + data = data.slice(0, MAX_PREVIEW_ROWS + 1); // +1 保留表头 + truncated = true; + } return { sheetName, data }; }); + setCsvTruncated(truncated); setExcelData(sheets); setLoading(false); return; } + const arrayBuffer = await fetchFileBuffer(fileUrl); const workbook = XLSX.read(arrayBuffer, { type: 'array' }); - const sheets = workbook.SheetNames.map(sheetName => { + const sheets = workbook.SheetNames.map((sheetName: string) => { const worksheet = workbook.Sheets[sheetName]; const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; return { sheetName, data }; @@ -522,9 +579,14 @@ const DocumentPreview: FC = ({ ) )} - {/* Excel 预览 */} + {/* Excel/CSV 预览 */} {isExcelFile() && !error && !loading && (
+ {csvTruncated && ( +
+ 文件较大,仅预览前 {MAX_PREVIEW_ROWS} 行数据 +
+ )} {excelData.map((sheet, index) => (

{sheet.sheetName}

@@ -541,6 +603,7 @@ const DocumentPreview: FC = ({ scroll={{ x: 'max-content' }} size="small" bordered + virtual /> )}
diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx index 08df86cd..dcee69dd 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx @@ -11,7 +11,7 @@ import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager'; import { Button, Spin, message, Switch } from 'antd'; -import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase'; +import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk, getFileUrl } from '@/api/knowledgeBase'; import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types'; import { formatDateTime } from '@/utils/format'; import InfoPanel, { type InfoItem } from '../components/InfoPanel'; @@ -138,7 +138,7 @@ const DocumentDetails: FC = () => { const response = await getDocumentDetail(documentId); setDocument(response); setInfoItems(formatDocumentInfo(response)); - const url = `${imagePath}/api/files/${response.file_id}` + const url = `${window.location.origin}/api/files/${response.file_id}`; setFileUrl(url); setParserMode(response?.parser_config?.auto_questions || 0) // ChunkList will be called automatically in useEffect based on document.progress