fix:office view
This commit is contained in:
@@ -52,6 +52,10 @@ export const getKnowledgeBaseTypeList = async (): Promise<string[]> => {
|
||||
// 如果不是数组,返回空数组
|
||||
return [];
|
||||
};
|
||||
// 获取文件地址
|
||||
export const getFileUrl = (fileId: string) => {
|
||||
return `${apiPrefix}/files/${fileId}`;
|
||||
};
|
||||
// 知识库文档解析类型
|
||||
export const getKnowledgeBaseDocumentParseTypeList = async () => {
|
||||
const response = await request.get(`${apiPrefix}/knowledges/parsertype`);
|
||||
|
||||
@@ -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<DocumentPreviewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<ArrayBuffer> => {
|
||||
const requestUrl = getRequestUrl(url);
|
||||
const headers: Record<string, string> = {
|
||||
'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<DocumentPreviewProps> = ({
|
||||
} 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<DocumentPreviewProps> = ({
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Excel 预览 */}
|
||||
{/* Excel/CSV 预览 */}
|
||||
{isExcelFile() && !error && !loading && (
|
||||
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
|
||||
{csvTruncated && (
|
||||
<div className="rb:mb-3 rb:px-3 rb:py-2 rb:bg-yellow-50 rb:border rb:border-yellow-200 rb:rounded rb:text-sm rb:text-yellow-700">
|
||||
文件较大,仅预览前 {MAX_PREVIEW_ROWS} 行数据
|
||||
</div>
|
||||
)}
|
||||
{excelData.map((sheet, index) => (
|
||||
<div key={index} className="rb:mb-6">
|
||||
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
|
||||
@@ -541,6 +603,7 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
||||
scroll={{ x: 'max-content' }}
|
||||
size="small"
|
||||
bordered
|
||||
virtual
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user