From 8f66c235fa5e2f05e729eeda36c6e62432d4d0ff Mon Sep 17 00:00:00 2001 From: dj <431634905@qq.com> Date: Fri, 10 Apr 2026 00:16:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B9=B6=E8=A1=8C?= =?UTF-8?q?=E5=A4=9A=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=B8=94=E5=9C=A8=E5=88=97=E8=A1=A8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8A=E4=BC=A0=E4=BA=86=E5=93=AA=E4=BA=9B=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E6=AC=A1=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Documents.tsx | 288 +++++++++++++++++++--------- frontend/src/pages/TemplateFill.tsx | 188 ++++++++++-------- 2 files changed, 312 insertions(+), 164 deletions(-) diff --git a/frontend/src/pages/Documents.tsx b/frontend/src/pages/Documents.tsx index d0d9c2e..168b8fe 100644 --- a/frontend/src/pages/Documents.tsx +++ b/frontend/src/pages/Documents.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useDropzone } from 'react-dropzone'; import { FileText, @@ -23,7 +23,8 @@ import { List, MessageSquareCode, Tag, - HelpCircle + HelpCircle, + Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -72,8 +73,10 @@ const Documents: React.FC = () => { // 上传相关状态 const [uploading, setUploading] = useState(false); const [uploadedFile, setUploadedFile] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState([]); const [parseResult, setParseResult] = useState(null); const [expandedSheet, setExpandedSheet] = useState(null); + const [uploadExpanded, setUploadExpanded] = useState(false); // AI 分析相关状态 const [analyzing, setAnalyzing] = useState(false); @@ -210,75 +213,119 @@ const Documents: React.FC = () => { // 文件上传处理 const onDrop = async (acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - if (!file) return; + if (acceptedFiles.length === 0) return; - setUploadedFile(file); setUploading(true); - setParseResult(null); - setAiAnalysis(null); - setAnalysisCharts(null); - setExpandedSheet(null); - setMdAnalysis(null); - setMdSections([]); - setMdStreamingContent(''); + let successCount = 0; + let failCount = 0; + const successfulFiles: File[] = []; - const ext = file.name.split('.').pop()?.toLowerCase(); + // 逐个上传文件 + for (const file of acceptedFiles) { + const ext = file.name.split('.').pop()?.toLowerCase(); - try { - // Excel 文件使用专门的上传接口 - if (ext === 'xlsx' || ext === 'xls') { - const result = await backendApi.uploadExcel(file, { - parseAllSheets: parseOptions.parseAllSheets, - headerRow: parseOptions.headerRow - }); - if (result.success) { - toast.success(`解析成功: ${file.name}`); - setParseResult(result); - loadDocuments(); // 刷新文档列表 - if (result.metadata?.sheet_count === 1) { - setExpandedSheet(Object.keys(result.data?.sheets || {})[0] || null); + try { + if (ext === 'xlsx' || ext === 'xls') { + const result = await backendApi.uploadExcel(file, { + parseAllSheets: parseOptions.parseAllSheets, + headerRow: parseOptions.headerRow + }); + if (result.success) { + successCount++; + successfulFiles.push(file); + // 第一个Excel文件设置解析结果供预览 + if (successCount === 1) { + setUploadedFile(file); + setParseResult(result); + if (result.metadata?.sheet_count === 1) { + setExpandedSheet(Object.keys(result.data?.sheets || {})[0] || null); + } + } + loadDocuments(); + } else { + failCount++; + toast.error(`${file.name}: ${result.error || '解析失败'}`); + } + } else if (ext === 'md' || ext === 'markdown') { + const result = await backendApi.uploadDocument(file); + if (result.task_id) { + successCount++; + successfulFiles.push(file); + if (successCount === 1) { + setUploadedFile(file); + } + // 轮询任务状态 + let attempts = 0; + const checkStatus = async () => { + while (attempts < 30) { + try { + const status = await backendApi.getTaskStatus(result.task_id); + if (status.status === 'success') { + loadDocuments(); + return; + } else if (status.status === 'failure') { + return; + } + } catch (e) { + console.error('检查状态失败', e); + } + await new Promise(resolve => setTimeout(resolve, 2000)); + attempts++; + } + }; + checkStatus(); + } else { + failCount++; } } else { - toast.error(result.error || '解析失败'); - } - } else if (ext === 'md' || ext === 'markdown') { - // Markdown 文件:获取大纲 - await fetchMdOutline(); - } else { - // 其他文档使用通用上传接口 - const result = await backendApi.uploadDocument(file); - if (result.task_id) { - toast.success(`文件 ${file.name} 已提交处理`); - // 轮询任务状态 - let attempts = 0; - const checkStatus = async () => { - while (attempts < 30) { - try { - const status = await backendApi.getTaskStatus(result.task_id); - if (status.status === 'success') { - toast.success(`文件 ${file.name} 处理完成`); - loadDocuments(); - return; - } else if (status.status === 'failure') { - toast.error(`文件 ${file.name} 处理失败`); - return; - } - } catch (e) { - console.error('检查状态失败', e); - } - await new Promise(resolve => setTimeout(resolve, 2000)); - attempts++; + // 其他文档使用通用上传接口 + const result = await backendApi.uploadDocument(file); + if (result.task_id) { + successCount++; + successfulFiles.push(file); + if (successCount === 1) { + setUploadedFile(file); } - toast.error(`文件 ${file.name} 处理超时`); - }; - checkStatus(); + // 轮询任务状态 + let attempts = 0; + const checkStatus = async () => { + while (attempts < 30) { + try { + const status = await backendApi.getTaskStatus(result.task_id); + if (status.status === 'success') { + loadDocuments(); + return; + } else if (status.status === 'failure') { + return; + } + } catch (e) { + console.error('检查状态失败', e); + } + await new Promise(resolve => setTimeout(resolve, 2000)); + attempts++; + } + }; + checkStatus(); + } else { + failCount++; + } } + } catch (error: any) { + failCount++; + toast.error(`${file.name}: ${error.message || '上传失败'}`); } - } catch (error: any) { - toast.error(error.message || '上传失败'); - } finally { - setUploading(false); + } + + setUploading(false); + loadDocuments(); + + if (successCount > 0) { + toast.success(`成功上传 ${successCount} 个文件`); + setUploadedFiles(prev => [...prev, ...successfulFiles]); + setUploadExpanded(true); + } + if (failCount > 0) { + toast.error(`${failCount} 个文件上传失败`); } }; @@ -291,7 +338,7 @@ const Documents: React.FC = () => { 'text/markdown': ['.md'], 'text/plain': ['.txt'] }, - maxFiles: 1 + multiple: true }); // AI 分析处理 @@ -449,6 +496,7 @@ const Documents: React.FC = () => { const handleDeleteFile = () => { setUploadedFile(null); + setUploadedFiles([]); setParseResult(null); setAiAnalysis(null); setAnalysisCharts(null); @@ -456,6 +504,17 @@ const Documents: React.FC = () => { toast.success('文件已清除'); }; + const handleRemoveUploadedFile = (index: number) => { + setUploadedFiles(prev => { + const newFiles = prev.filter((_, i) => i !== index); + if (newFiles.length === 0) { + setUploadedFile(null); + } + return newFiles; + }); + toast.success('文件已从列表移除'); + }; + const handleDelete = async (docId: string) => { try { const result = await backendApi.deleteDocument(docId); @@ -640,7 +699,82 @@ const Documents: React.FC = () => { {uploadPanelOpen && ( - {!uploadedFile ? ( + {uploadedFiles.length > 0 || uploadedFile ? ( +
+ {/* 文件列表头部 */} +
setUploadExpanded(!uploadExpanded)} + > +
+
+ +
+
+

+ 已上传 {(uploadedFiles.length > 0 ? uploadedFiles : [uploadedFile]).length} 个文件 +

+

+ {uploadExpanded ? '点击收起' : '点击展开查看'} +

+
+
+
+ + {uploadExpanded ? : } +
+
+ + {/* 展开的文件列表 */} + {uploadExpanded && ( +
+ {(uploadedFiles.length > 0 ? uploadedFiles : [uploadedFile]).filter(Boolean).map((file, index) => ( +
+
+ {isExcelFile(file?.name || '') ? : } +
+
+

{file?.name}

+

{formatFileSize(file?.size || 0)}

+
+ +
+ ))} + + {/* 继续添加按钮 */} +
+ + + 继续添加更多文件 +
+
+ )} +
+ ) : (
{ uploading && "opacity-50 pointer-events-none" )} > - +
{uploading ? : }
@@ -671,30 +805,6 @@ const Documents: React.FC = () => {
- ) : ( -
-
-
- {isExcelFile(uploadedFile.name) ? : } -
-
-

{uploadedFile.name}

-

{formatFileSize(uploadedFile.size)}

-
- -
- - {isExcelFile(uploadedFile.name) && ( - - )} -
)}
)} diff --git a/frontend/src/pages/TemplateFill.tsx b/frontend/src/pages/TemplateFill.tsx index 6b96bbd..0f7fe88 100644 --- a/frontend/src/pages/TemplateFill.tsx +++ b/frontend/src/pages/TemplateFill.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useDropzone } from 'react-dropzone'; import { TableProperties, @@ -18,7 +18,8 @@ import { Files, Trash2, Eye, - File + File, + Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; @@ -72,6 +73,7 @@ const TemplateFill: React.FC = () => { const [sourceMode, setSourceMode] = useState<'upload' | 'select'>('upload'); const [uploadedDocuments, setUploadedDocuments] = useState([]); const [docsLoading, setDocsLoading] = useState(false); + const sourceFileInputRef = useRef(null); // 模板拖拽 const onTemplateDrop = useCallback((acceptedFiles: File[]) => { @@ -93,25 +95,34 @@ const TemplateFill: React.FC = () => { }); // 源文档拖拽 - const onSourceDrop = useCallback((acceptedFiles: File[]) => { - const newFiles = acceptedFiles.map(f => ({ - file: f, - preview: f.type.startsWith('text/') || f.name.endsWith('.md') ? undefined : undefined - })); - addSourceFiles(newFiles); + const onSourceDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + const files = Array.from(e.dataTransfer.files).filter(f => { + const ext = f.name.split('.').pop()?.toLowerCase(); + return ['xlsx', 'xls', 'docx', 'md', 'txt'].includes(ext || ''); + }); + if (files.length > 0) { + addSourceFiles(files.map(f => ({ file: f }))); + } }, [addSourceFiles]); - const { getRootProps: getSourceProps, getInputProps: getSourceInputProps, isDragActive: isSourceDragActive } = useDropzone({ - onDrop: onSourceDrop, - accept: { - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'text/plain': ['.txt'], - 'text/markdown': ['.md'] - }, - multiple: true - }); + const handleSourceFileSelect = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + addSourceFiles(files.map(f => ({ file: f }))); + toast.success(`已添加 ${files.length} 个文件`); + } + e.target.value = ''; + }; + + // 仅添加源文档不上传 + const handleAddSourceFiles = () => { + if (sourceFiles.length === 0) { + toast.error('请先选择源文档'); + return; + } + toast.success(`已添加 ${sourceFiles.length} 个源文档,可继续添加更多`); + }; // 加载已上传文档 const loadUploadedDocuments = useCallback(async () => { @@ -371,23 +382,33 @@ const TemplateFill: React.FC = () => { {sourceMode === 'upload' ? ( <> +
+ + +
{ e.preventDefault(); }} + onDrop={onSourceDrop} + className="mt-2 text-center text-xs text-muted-foreground" > - -
- {loading ? : } -
-

- {isSourceDragActive ? '释放以上传' : '点击或拖拽上传源文档'} -

-

- 支持 .xlsx .xls .docx .md .txt -

+ 或拖拽文件到此处
{/* Selected Source Files */} @@ -407,6 +428,12 @@ const TemplateFill: React.FC = () => { ))} +
+ +
)} @@ -420,49 +447,60 @@ const TemplateFill: React.FC = () => { ))} ) : uploadedDocuments.length > 0 ? ( -
- {uploadedDocuments.map((doc) => ( -
{ - if (sourceDocIds.includes(doc.doc_id)) { - removeSourceDocId(doc.doc_id); - } else { - addSourceDocId(doc.doc_id); - } - }} - > -
- {sourceDocIds.includes(doc.doc_id) && } -
- {getFileIcon(doc.original_filename)} -
-

{doc.original_filename}

-

- {doc.doc_type.toUpperCase()} • {format(new Date(doc.created_at), 'yyyy-MM-dd')} -

-
-
- ))} + )} +
+ {uploadedDocuments.map((doc) => ( +
{ + if (sourceDocIds.includes(doc.doc_id)) { + removeSourceDocId(doc.doc_id); + } else { + addSourceDocId(doc.doc_id); + } + }} + > +
+ {sourceDocIds.includes(doc.doc_id) && } +
+ {getFileIcon(doc.original_filename)} +
+

{doc.original_filename}

+

+ {doc.doc_type.toUpperCase()} • {format(new Date(doc.created_at), 'yyyy-MM-dd')} +

+
+ +
+ ))} +
) : (
From 6befc510d8308aca80a9b73a2231bf70d875c53d Mon Sep 17 00:00:00 2001 From: dj <431634905@qq.com> Date: Fri, 10 Apr 2026 00:23:23 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=88=B7=E6=96=B0=E7=9A=84debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Documents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Documents.tsx b/frontend/src/pages/Documents.tsx index 168b8fe..afeb54d 100644 --- a/frontend/src/pages/Documents.tsx +++ b/frontend/src/pages/Documents.tsx @@ -674,7 +674,7 @@ const Documents: React.FC = () => {

文档中心

上传文档,自动解析并使用 AI 进行深度分析

-