Merge branch 'refs/heads/release/v0.2.8' into fix/features_028
This commit is contained in:
@@ -52,6 +52,10 @@ export const getKnowledgeBaseTypeList = async (): Promise<string[]> => {
|
|||||||
// 如果不是数组,返回空数组
|
// 如果不是数组,返回空数组
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
// 获取文件地址
|
||||||
|
export const getFileUrl = (fileId: string) => {
|
||||||
|
return `${apiPrefix}/files/${fileId}`;
|
||||||
|
};
|
||||||
// 知识库文档解析类型
|
// 知识库文档解析类型
|
||||||
export const getKnowledgeBaseDocumentParseTypeList = async () => {
|
export const getKnowledgeBaseDocumentParseTypeList = async () => {
|
||||||
const response = await request.get(`${apiPrefix}/knowledges/parsertype`);
|
const response = await request.get(`${apiPrefix}/knowledges/parsertype`);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @Author: yujiangping
|
* @Author: yujiangping
|
||||||
* @Date: 2026-03-16 19:01:12
|
* @Date: 2026-03-16 19:01:12
|
||||||
* @LastEditors: yujiangping
|
* @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 { useState, useEffect, useRef, useCallback, type FC } from 'react';
|
||||||
import { Spin, Alert, Button, Table, InputNumber, Image } from 'antd';
|
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';
|
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 () => {
|
const loadExcelFile = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
setCsvTruncated(false);
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
// CSV 文件需要处理编码问题(可能是 GBK/GB2312),且大文件只取前 1MB
|
||||||
|
|
||||||
// CSV 文件需要处理编码问题(可能是 GBK/GB2312)
|
|
||||||
if (isCsvFile()) {
|
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;
|
let csvText: string;
|
||||||
// 先尝试 UTF-8 解码
|
|
||||||
const utf8Text = new TextDecoder('utf-8').decode(arrayBuffer);
|
const utf8Text = new TextDecoder('utf-8').decode(arrayBuffer);
|
||||||
// 检测是否有乱码特征(常见的 GBK 被错误解析为 UTF-8 的替换字符)
|
|
||||||
if (utf8Text.includes('\uFFFD') || /[\x80-\xff]/.test(utf8Text.slice(0, 200))) {
|
if (utf8Text.includes('\uFFFD') || /[\x80-\xff]/.test(utf8Text.slice(0, 200))) {
|
||||||
// 尝试 GBK 解码
|
|
||||||
try {
|
try {
|
||||||
csvText = new TextDecoder('gbk').decode(arrayBuffer);
|
csvText = new TextDecoder('gbk').decode(arrayBuffer);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -334,19 +375,35 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
csvText = utf8Text;
|
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 workbook = XLSX.read(csvText, { type: 'string' });
|
||||||
const sheets = workbook.SheetNames.map(sheetName => {
|
const sheets = workbook.SheetNames.map(sheetName => {
|
||||||
const worksheet = workbook.Sheets[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 };
|
return { sheetName, data };
|
||||||
});
|
});
|
||||||
|
setCsvTruncated(truncated);
|
||||||
setExcelData(sheets);
|
setExcelData(sheets);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
||||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
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 worksheet = workbook.Sheets[sheetName];
|
||||||
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||||
return { sheetName, data };
|
return { sheetName, data };
|
||||||
@@ -522,9 +579,14 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Excel 预览 */}
|
{/* Excel/CSV 预览 */}
|
||||||
{isExcelFile() && !error && !loading && (
|
{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">
|
<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) => (
|
{excelData.map((sheet, index) => (
|
||||||
<div key={index} className="rb:mb-6">
|
<div key={index} className="rb:mb-6">
|
||||||
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
|
<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' }}
|
scroll={{ x: 'max-content' }}
|
||||||
size="small"
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
|
virtual
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
|
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
|
||||||
import { Button, Spin, message, Switch } from 'antd';
|
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 type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types';
|
||||||
import { formatDateTime } from '@/utils/format';
|
import { formatDateTime } from '@/utils/format';
|
||||||
import InfoPanel, { type InfoItem } from '../components/InfoPanel';
|
import InfoPanel, { type InfoItem } from '../components/InfoPanel';
|
||||||
@@ -138,7 +138,7 @@ const DocumentDetails: FC = () => {
|
|||||||
const response = await getDocumentDetail(documentId);
|
const response = await getDocumentDetail(documentId);
|
||||||
setDocument(response);
|
setDocument(response);
|
||||||
setInfoItems(formatDocumentInfo(response));
|
setInfoItems(formatDocumentInfo(response));
|
||||||
const url = `${imagePath}/api/files/${response.file_id}`
|
const url = `${window.location.origin}/api/files/${response.file_id}`;
|
||||||
setFileUrl(url);
|
setFileUrl(url);
|
||||||
setParserMode(response?.parser_config?.auto_questions || 0)
|
setParserMode(response?.parser_config?.auto_questions || 0)
|
||||||
// ChunkList will be called automatically in useEffect based on document.progress
|
// ChunkList will be called automatically in useEffect based on document.progress
|
||||||
|
|||||||
Reference in New Issue
Block a user