Merge branch 'main' of https://gitea.kronecker.cc/OurCodesAreAllRight/FilesReadSystem
This commit is contained in:
@@ -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 { useDropzone } from 'react-dropzone';
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
List,
|
List,
|
||||||
MessageSquareCode,
|
MessageSquareCode,
|
||||||
Tag,
|
Tag,
|
||||||
HelpCircle
|
HelpCircle,
|
||||||
|
Plus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -72,8 +73,10 @@ const Documents: React.FC = () => {
|
|||||||
// 上传相关状态
|
// 上传相关状态
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
||||||
const [parseResult, setParseResult] = useState<ExcelParseResult | null>(null);
|
const [parseResult, setParseResult] = useState<ExcelParseResult | null>(null);
|
||||||
const [expandedSheet, setExpandedSheet] = useState<string | null>(null);
|
const [expandedSheet, setExpandedSheet] = useState<string | null>(null);
|
||||||
|
const [uploadExpanded, setUploadExpanded] = useState(false);
|
||||||
|
|
||||||
// AI 分析相关状态
|
// AI 分析相关状态
|
||||||
const [analyzing, setAnalyzing] = useState(false);
|
const [analyzing, setAnalyzing] = useState(false);
|
||||||
@@ -210,75 +213,119 @@ const Documents: React.FC = () => {
|
|||||||
|
|
||||||
// 文件上传处理
|
// 文件上传处理
|
||||||
const onDrop = async (acceptedFiles: File[]) => {
|
const onDrop = async (acceptedFiles: File[]) => {
|
||||||
const file = acceptedFiles[0];
|
if (acceptedFiles.length === 0) return;
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
setUploadedFile(file);
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setParseResult(null);
|
let successCount = 0;
|
||||||
setAiAnalysis(null);
|
let failCount = 0;
|
||||||
setAnalysisCharts(null);
|
const successfulFiles: File[] = [];
|
||||||
setExpandedSheet(null);
|
|
||||||
setMdAnalysis(null);
|
|
||||||
setMdSections([]);
|
|
||||||
setMdStreamingContent('');
|
|
||||||
|
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
// 逐个上传文件
|
||||||
|
for (const file of acceptedFiles) {
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Excel 文件使用专门的上传接口
|
if (ext === 'xlsx' || ext === 'xls') {
|
||||||
if (ext === 'xlsx' || ext === 'xls') {
|
const result = await backendApi.uploadExcel(file, {
|
||||||
const result = await backendApi.uploadExcel(file, {
|
parseAllSheets: parseOptions.parseAllSheets,
|
||||||
parseAllSheets: parseOptions.parseAllSheets,
|
headerRow: parseOptions.headerRow
|
||||||
headerRow: parseOptions.headerRow
|
});
|
||||||
});
|
if (result.success) {
|
||||||
if (result.success) {
|
successCount++;
|
||||||
toast.success(`解析成功: ${file.name}`);
|
successfulFiles.push(file);
|
||||||
setParseResult(result);
|
// 第一个Excel文件设置解析结果供预览
|
||||||
loadDocuments(); // 刷新文档列表
|
if (successCount === 1) {
|
||||||
if (result.metadata?.sheet_count === 1) {
|
setUploadedFile(file);
|
||||||
setExpandedSheet(Object.keys(result.data?.sheets || {})[0] || null);
|
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 {
|
} else {
|
||||||
toast.error(result.error || '解析失败');
|
// 其他文档使用通用上传接口
|
||||||
}
|
const result = await backendApi.uploadDocument(file);
|
||||||
} else if (ext === 'md' || ext === 'markdown') {
|
if (result.task_id) {
|
||||||
// Markdown 文件:获取大纲
|
successCount++;
|
||||||
await fetchMdOutline();
|
successfulFiles.push(file);
|
||||||
} else {
|
if (successCount === 1) {
|
||||||
// 其他文档使用通用上传接口
|
setUploadedFile(file);
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
toast.error(`文件 ${file.name} 处理超时`);
|
// 轮询任务状态
|
||||||
};
|
let attempts = 0;
|
||||||
checkStatus();
|
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/markdown': ['.md'],
|
||||||
'text/plain': ['.txt']
|
'text/plain': ['.txt']
|
||||||
},
|
},
|
||||||
maxFiles: 1
|
multiple: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// AI 分析处理
|
// AI 分析处理
|
||||||
@@ -449,6 +496,7 @@ const Documents: React.FC = () => {
|
|||||||
|
|
||||||
const handleDeleteFile = () => {
|
const handleDeleteFile = () => {
|
||||||
setUploadedFile(null);
|
setUploadedFile(null);
|
||||||
|
setUploadedFiles([]);
|
||||||
setParseResult(null);
|
setParseResult(null);
|
||||||
setAiAnalysis(null);
|
setAiAnalysis(null);
|
||||||
setAnalysisCharts(null);
|
setAnalysisCharts(null);
|
||||||
@@ -456,6 +504,17 @@ const Documents: React.FC = () => {
|
|||||||
toast.success('文件已清除');
|
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) => {
|
const handleDelete = async (docId: string) => {
|
||||||
try {
|
try {
|
||||||
const result = await backendApi.deleteDocument(docId);
|
const result = await backendApi.deleteDocument(docId);
|
||||||
@@ -615,7 +674,7 @@ const Documents: React.FC = () => {
|
|||||||
<h1 className="text-3xl font-extrabold tracking-tight">文档中心</h1>
|
<h1 className="text-3xl font-extrabold tracking-tight">文档中心</h1>
|
||||||
<p className="text-muted-foreground">上传文档,自动解析并使用 AI 进行深度分析</p>
|
<p className="text-muted-foreground">上传文档,自动解析并使用 AI 进行深度分析</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="rounded-xl gap-2" onClick={loadDocuments}>
|
<Button variant="outline" className="rounded-xl gap-2" onClick={() => loadDocuments()}>
|
||||||
<RefreshCcw size={18} />
|
<RefreshCcw size={18} />
|
||||||
<span>刷新</span>
|
<span>刷新</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -640,7 +699,82 @@ const Documents: React.FC = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
{uploadPanelOpen && (
|
{uploadPanelOpen && (
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{!uploadedFile ? (
|
{uploadedFiles.length > 0 || uploadedFile ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 文件列表头部 */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between p-3 bg-muted/50 rounded-xl cursor-pointer hover:bg-muted/70 transition-colors"
|
||||||
|
onClick={() => setUploadExpanded(!uploadExpanded)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
|
||||||
|
<Upload size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-sm">
|
||||||
|
已上传 {(uploadedFiles.length > 0 ? uploadedFiles : [uploadedFile]).length} 个文件
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{uploadExpanded ? '点击收起' : '点击展开查看'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFile();
|
||||||
|
}}
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="mr-1" />
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
{uploadExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 展开的文件列表 */}
|
||||||
|
{uploadExpanded && (
|
||||||
|
<div className="space-y-2 border rounded-xl p-3">
|
||||||
|
{(uploadedFiles.length > 0 ? uploadedFiles : [uploadedFile]).filter(Boolean).map((file, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-3 p-2 bg-background rounded-lg">
|
||||||
|
<div className={cn(
|
||||||
|
"w-8 h-8 rounded flex items-center justify-center",
|
||||||
|
isExcelFile(file?.name || '') ? "bg-emerald-500/10 text-emerald-500" : "bg-blue-500/10 text-blue-500"
|
||||||
|
)}>
|
||||||
|
{isExcelFile(file?.name || '') ? <FileSpreadsheet size={16} /> : <FileText size={16} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm truncate">{file?.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatFileSize(file?.size || 0)}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => handleRemoveUploadedFile(index)}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 继续添加按钮 */}
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className="flex items-center justify-center gap-2 p-3 border-2 border-dashed rounded-lg cursor-pointer hover:border-primary/50 hover:bg-primary/5 transition-colors"
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} multiple={true} />
|
||||||
|
<Plus size={16} className="text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">继续添加更多文件</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -649,7 +783,7 @@ const Documents: React.FC = () => {
|
|||||||
uploading && "opacity-50 pointer-events-none"
|
uploading && "opacity-50 pointer-events-none"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} multiple={true} />
|
||||||
<div className="w-14 h-14 rounded-xl bg-primary/10 text-primary flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
<div className="w-14 h-14 rounded-xl bg-primary/10 text-primary flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||||
{uploading ? <Loader2 className="animate-spin" size={28} /> : <Upload size={28} />}
|
{uploading ? <Loader2 className="animate-spin" size={28} /> : <Upload size={28} />}
|
||||||
</div>
|
</div>
|
||||||
@@ -671,30 +805,6 @@ const Documents: React.FC = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-muted/30 rounded-xl">
|
|
||||||
<div className={cn(
|
|
||||||
"w-10 h-10 rounded-lg flex items-center justify-center",
|
|
||||||
isExcelFile(uploadedFile.name) ? "bg-emerald-500/10 text-emerald-500" : "bg-blue-500/10 text-blue-500"
|
|
||||||
)}>
|
|
||||||
{isExcelFile(uploadedFile.name) ? <FileSpreadsheet size={20} /> : <FileText size={20} />}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="font-semibold text-sm truncate">{uploadedFile.name}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{formatFileSize(uploadedFile.size)}</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="icon" className="text-destructive hover:bg-destructive/10" onClick={handleDeleteFile}>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExcelFile(uploadedFile.name) && (
|
|
||||||
<Button onClick={() => onDrop([uploadedFile])} className="w-full" disabled={uploading}>
|
|
||||||
{uploading ? '解析中...' : '重新解析'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 { useDropzone } from 'react-dropzone';
|
||||||
import {
|
import {
|
||||||
TableProperties,
|
TableProperties,
|
||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
Files,
|
Files,
|
||||||
Trash2,
|
Trash2,
|
||||||
Eye,
|
Eye,
|
||||||
File
|
File,
|
||||||
|
Plus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
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 [sourceMode, setSourceMode] = useState<'upload' | 'select'>('upload');
|
||||||
const [uploadedDocuments, setUploadedDocuments] = useState<DocumentItem[]>([]);
|
const [uploadedDocuments, setUploadedDocuments] = useState<DocumentItem[]>([]);
|
||||||
const [docsLoading, setDocsLoading] = useState(false);
|
const [docsLoading, setDocsLoading] = useState(false);
|
||||||
|
const sourceFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 模板拖拽
|
// 模板拖拽
|
||||||
const onTemplateDrop = useCallback((acceptedFiles: File[]) => {
|
const onTemplateDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
@@ -93,25 +95,34 @@ const TemplateFill: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 源文档拖拽
|
// 源文档拖拽
|
||||||
const onSourceDrop = useCallback((acceptedFiles: File[]) => {
|
const onSourceDrop = useCallback((e: React.DragEvent) => {
|
||||||
const newFiles = acceptedFiles.map(f => ({
|
e.preventDefault();
|
||||||
file: f,
|
const files = Array.from(e.dataTransfer.files).filter(f => {
|
||||||
preview: f.type.startsWith('text/') || f.name.endsWith('.md') ? undefined : undefined
|
const ext = f.name.split('.').pop()?.toLowerCase();
|
||||||
}));
|
return ['xlsx', 'xls', 'docx', 'md', 'txt'].includes(ext || '');
|
||||||
addSourceFiles(newFiles);
|
});
|
||||||
|
if (files.length > 0) {
|
||||||
|
addSourceFiles(files.map(f => ({ file: f })));
|
||||||
|
}
|
||||||
}, [addSourceFiles]);
|
}, [addSourceFiles]);
|
||||||
|
|
||||||
const { getRootProps: getSourceProps, getInputProps: getSourceInputProps, isDragActive: isSourceDragActive } = useDropzone({
|
const handleSourceFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onDrop: onSourceDrop,
|
const files = Array.from(e.target.files || []);
|
||||||
accept: {
|
if (files.length > 0) {
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
|
addSourceFiles(files.map(f => ({ file: f })));
|
||||||
'application/vnd.ms-excel': ['.xls'],
|
toast.success(`已添加 ${files.length} 个文件`);
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
}
|
||||||
'text/plain': ['.txt'],
|
e.target.value = '';
|
||||||
'text/markdown': ['.md']
|
};
|
||||||
},
|
|
||||||
multiple: true
|
// 仅添加源文档不上传
|
||||||
});
|
const handleAddSourceFiles = () => {
|
||||||
|
if (sourceFiles.length === 0) {
|
||||||
|
toast.error('请先选择源文档');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(`已添加 ${sourceFiles.length} 个源文档,可继续添加更多`);
|
||||||
|
};
|
||||||
|
|
||||||
// 加载已上传文档
|
// 加载已上传文档
|
||||||
const loadUploadedDocuments = useCallback(async () => {
|
const loadUploadedDocuments = useCallback(async () => {
|
||||||
@@ -371,23 +382,33 @@ const TemplateFill: React.FC = () => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{sourceMode === 'upload' ? (
|
{sourceMode === 'upload' ? (
|
||||||
<>
|
<>
|
||||||
|
<div className="border-2 border-dashed rounded-2xl p-8 transition-all duration-300 flex flex-col items-center justify-center text-center cursor-pointer group min-h-[200px] border-muted-foreground/20 hover:border-primary/50 hover:bg-primary/5">
|
||||||
|
<input
|
||||||
|
id="source-file-input"
|
||||||
|
type="file"
|
||||||
|
multiple={true}
|
||||||
|
accept=".xlsx,.xls,.docx,.md,.txt"
|
||||||
|
onChange={handleSourceFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<label htmlFor="source-file-input" className="cursor-pointer flex flex-col items-center">
|
||||||
|
<div className="w-14 h-14 rounded-xl bg-blue-500/10 text-blue-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||||
|
{loading ? <Loader2 className="animate-spin" size={28} /> : <Upload size={28} />}
|
||||||
|
</div>
|
||||||
|
<p className="font-medium">
|
||||||
|
点击上传源文档
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
支持 .xlsx .xls .docx .md .txt
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
{...getSourceProps()}
|
onDragOver={(e) => { e.preventDefault(); }}
|
||||||
className={cn(
|
onDrop={onSourceDrop}
|
||||||
"border-2 border-dashed rounded-2xl p-8 transition-all duration-300 flex flex-col items-center justify-center text-center cursor-pointer group min-h-[200px]",
|
className="mt-2 text-center text-xs text-muted-foreground"
|
||||||
isSourceDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/20 hover:border-primary/50 hover:bg-primary/5"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<input {...getSourceInputProps()} />
|
或拖拽文件到此处
|
||||||
<div className="w-14 h-14 rounded-xl bg-blue-500/10 text-blue-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
|
||||||
{loading ? <Loader2 className="animate-spin" size={28} /> : <Upload size={28} />}
|
|
||||||
</div>
|
|
||||||
<p className="font-medium">
|
|
||||||
{isSourceDragActive ? '释放以上传' : '点击或拖拽上传源文档'}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
支持 .xlsx .xls .docx .md .txt
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Selected Source Files */}
|
{/* Selected Source Files */}
|
||||||
@@ -407,6 +428,12 @@ const TemplateFill: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div className="flex justify-center pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => document.getElementById('source-file-input')?.click()}>
|
||||||
|
<Plus size={14} className="mr-1" />
|
||||||
|
继续添加更多文档
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -420,49 +447,60 @@ const TemplateFill: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : uploadedDocuments.length > 0 ? (
|
) : uploadedDocuments.length > 0 ? (
|
||||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
<div className="space-y-2">
|
||||||
{uploadedDocuments.map((doc) => (
|
{sourceDocIds.length > 0 && (
|
||||||
<div
|
<div className="flex items-center justify-between p-3 bg-primary/5 rounded-xl border border-primary/20">
|
||||||
key={doc.doc_id}
|
<span className="text-sm font-medium">已选择 {sourceDocIds.length} 个文档</span>
|
||||||
className={cn(
|
<Button variant="ghost" size="sm" onClick={() => loadUploadedDocuments()}>
|
||||||
"flex items-center gap-3 p-3 rounded-xl border-2 transition-all cursor-pointer",
|
<RefreshCcw size={14} className="mr-1" />
|
||||||
sourceDocIds.includes(doc.doc_id)
|
刷新列表
|
||||||
? "border-primary bg-primary/5"
|
|
||||||
: "border-border hover:bg-muted/30"
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (sourceDocIds.includes(doc.doc_id)) {
|
|
||||||
removeSourceDocId(doc.doc_id);
|
|
||||||
} else {
|
|
||||||
addSourceDocId(doc.doc_id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={cn(
|
|
||||||
"w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all shrink-0",
|
|
||||||
sourceDocIds.includes(doc.doc_id)
|
|
||||||
? "border-primary bg-primary text-white"
|
|
||||||
: "border-muted-foreground/30"
|
|
||||||
)}>
|
|
||||||
{sourceDocIds.includes(doc.doc_id) && <CheckCircle2 size={14} />}
|
|
||||||
</div>
|
|
||||||
{getFileIcon(doc.original_filename)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium truncate">{doc.original_filename}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{doc.doc_type.toUpperCase()} • {format(new Date(doc.created_at), 'yyyy-MM-dd')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => handleDeleteDocument(doc.doc_id, e)}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
<Trash2 size={14} className="text-red-500" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
<div className="max-h-[300px] overflow-y-auto space-y-2">
|
||||||
|
{uploadedDocuments.map((doc) => (
|
||||||
|
<div
|
||||||
|
key={doc.doc_id}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 p-3 rounded-xl border-2 transition-all cursor-pointer",
|
||||||
|
sourceDocIds.includes(doc.doc_id)
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-border hover:bg-muted/30"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (sourceDocIds.includes(doc.doc_id)) {
|
||||||
|
removeSourceDocId(doc.doc_id);
|
||||||
|
} else {
|
||||||
|
addSourceDocId(doc.doc_id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-6 h-6 rounded-md border-2 flex items-center justify-center transition-all shrink-0",
|
||||||
|
sourceDocIds.includes(doc.doc_id)
|
||||||
|
? "border-primary bg-primary text-white"
|
||||||
|
: "border-muted-foreground/30"
|
||||||
|
)}>
|
||||||
|
{sourceDocIds.includes(doc.doc_id) && <CheckCircle2 size={14} />}
|
||||||
|
</div>
|
||||||
|
{getFileIcon(doc.original_filename)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{doc.original_filename}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{doc.doc_type.toUpperCase()} • {format(new Date(doc.created_at), 'yyyy-MM-dd')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => handleDeleteDocument(doc.doc_id, e)}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user