新增从文档中心选择源文档功能及删除功能
智能填表模块新增"从文档中心选择"模式,支持选择已上传的文档作为数据源, 同时支持从列表中删除文档。两种模式通过Tab切换。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ interface TemplateFillState {
|
||||
templateFields: TemplateField[];
|
||||
sourceFiles: SourceFile[];
|
||||
sourceFilePaths: string[];
|
||||
sourceDocIds: string[];
|
||||
templateId: string;
|
||||
filledResult: any;
|
||||
setStep: (step: Step) => void;
|
||||
@@ -30,6 +31,9 @@ interface TemplateFillState {
|
||||
addSourceFiles: (files: SourceFile[]) => void;
|
||||
removeSourceFile: (index: number) => void;
|
||||
setSourceFilePaths: (paths: string[]) => void;
|
||||
setSourceDocIds: (ids: string[]) => void;
|
||||
addSourceDocId: (id: string) => void;
|
||||
removeSourceDocId: (id: string) => void;
|
||||
setTemplateId: (id: string) => void;
|
||||
setFilledResult: (result: any) => void;
|
||||
reset: () => void;
|
||||
@@ -41,6 +45,7 @@ const initialState = {
|
||||
templateFields: [],
|
||||
sourceFiles: [],
|
||||
sourceFilePaths: [],
|
||||
sourceDocIds: [],
|
||||
templateId: '',
|
||||
filledResult: null,
|
||||
setStep: () => {},
|
||||
@@ -50,6 +55,9 @@ const initialState = {
|
||||
addSourceFiles: () => {},
|
||||
removeSourceFile: () => {},
|
||||
setSourceFilePaths: () => {},
|
||||
setSourceDocIds: () => {},
|
||||
addSourceDocId: () => {},
|
||||
removeSourceDocId: () => {},
|
||||
setTemplateId: () => {},
|
||||
setFilledResult: () => {},
|
||||
reset: () => {},
|
||||
@@ -63,6 +71,7 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
const [templateFields, setTemplateFields] = useState<TemplateField[]>([]);
|
||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||
const [sourceFilePaths, setSourceFilePaths] = useState<string[]>([]);
|
||||
const [sourceDocIds, setSourceDocIds] = useState<string[]>([]);
|
||||
const [templateId, setTemplateId] = useState<string>('');
|
||||
const [filledResult, setFilledResult] = useState<any>(null);
|
||||
|
||||
@@ -74,12 +83,21 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
setSourceFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const addSourceDocId = (id: string) => {
|
||||
setSourceDocIds(prev => prev.includes(id) ? prev : [...prev, id]);
|
||||
};
|
||||
|
||||
const removeSourceDocId = (id: string) => {
|
||||
setSourceDocIds(prev => prev.filter(docId => docId !== id));
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setStep('upload');
|
||||
setTemplateFile(null);
|
||||
setTemplateFields([]);
|
||||
setSourceFiles([]);
|
||||
setSourceFilePaths([]);
|
||||
setSourceDocIds([]);
|
||||
setTemplateId('');
|
||||
setFilledResult(null);
|
||||
};
|
||||
@@ -92,6 +110,7 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
templateFields,
|
||||
sourceFiles,
|
||||
sourceFilePaths,
|
||||
sourceDocIds,
|
||||
templateId,
|
||||
filledResult,
|
||||
setStep,
|
||||
@@ -101,6 +120,9 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||
addSourceFiles,
|
||||
removeSourceFile,
|
||||
setSourceFilePaths,
|
||||
setSourceDocIds,
|
||||
addSourceDocId,
|
||||
removeSourceDocId,
|
||||
setTemplateId,
|
||||
setFilledResult,
|
||||
reset,
|
||||
|
||||
@@ -60,6 +60,7 @@ const TemplateFill: React.FC = () => {
|
||||
templateFields, setTemplateFields,
|
||||
sourceFiles, setSourceFiles, addSourceFiles, removeSourceFile,
|
||||
sourceFilePaths, setSourceFilePaths,
|
||||
sourceDocIds, setSourceDocIds, addSourceDocId, removeSourceDocId,
|
||||
templateId, setTemplateId,
|
||||
filledResult, setFilledResult,
|
||||
reset
|
||||
@@ -68,6 +69,9 @@ const TemplateFill: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [sourceMode, setSourceMode] = useState<'upload' | 'select'>('upload');
|
||||
const [uploadedDocuments, setUploadedDocuments] = useState<DocumentItem[]>([]);
|
||||
const [docsLoading, setDocsLoading] = useState(false);
|
||||
|
||||
// 模板拖拽
|
||||
const onTemplateDrop = useCallback((acceptedFiles: File[]) => {
|
||||
@@ -109,40 +113,118 @@ const TemplateFill: React.FC = () => {
|
||||
multiple: true
|
||||
});
|
||||
|
||||
// 加载已上传文档
|
||||
const loadUploadedDocuments = useCallback(async () => {
|
||||
setDocsLoading(true);
|
||||
try {
|
||||
const result = await backendApi.getDocuments(undefined, 100);
|
||||
if (result.success) {
|
||||
// 过滤可作为数据源的文档类型
|
||||
const docs = (result.documents || []).filter((d: DocumentItem) =>
|
||||
['docx', 'md', 'txt', 'xlsx', 'xls'].includes(d.doc_type)
|
||||
);
|
||||
setUploadedDocuments(docs);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('加载文档失败:', err);
|
||||
} finally {
|
||||
setDocsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 删除文档
|
||||
const handleDeleteDocument = async (docId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('确定要删除该文档吗?')) return;
|
||||
try {
|
||||
const result = await backendApi.deleteDocument(docId);
|
||||
if (result.success) {
|
||||
setUploadedDocuments(prev => prev.filter(d => d.doc_id !== docId));
|
||||
removeSourceDocId(docId);
|
||||
toast.success('文档已删除');
|
||||
} else {
|
||||
toast.error(result.message || '删除失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('删除失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceMode === 'select') {
|
||||
loadUploadedDocuments();
|
||||
}
|
||||
}, [sourceMode, loadUploadedDocuments]);
|
||||
|
||||
const handleJointUploadAndFill = async () => {
|
||||
if (!templateFile) {
|
||||
toast.error('请先上传模板文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了数据源
|
||||
if (sourceMode === 'upload' && sourceFiles.length === 0) {
|
||||
toast.error('请上传源文档或从已上传文档中选择');
|
||||
return;
|
||||
}
|
||||
if (sourceMode === 'select' && sourceDocIds.length === 0) {
|
||||
toast.error('请选择源文档');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 使用联合上传API
|
||||
const result = await backendApi.uploadTemplateAndSources(
|
||||
templateFile,
|
||||
sourceFiles.map(sf => sf.file)
|
||||
);
|
||||
if (sourceMode === 'select') {
|
||||
// 使用已上传文档作为数据源
|
||||
const result = await backendApi.uploadTemplate(templateFile);
|
||||
|
||||
if (result.success) {
|
||||
setTemplateFields(result.fields || []);
|
||||
setTemplateId(result.template_id);
|
||||
setSourceFilePaths(result.source_file_paths || []);
|
||||
toast.success('文档上传成功,开始智能填表');
|
||||
setStep('filling');
|
||||
if (result.success) {
|
||||
setTemplateFields(result.fields || []);
|
||||
setTemplateId(result.template_id || 'temp');
|
||||
toast.success('开始智能填表');
|
||||
setStep('filling');
|
||||
|
||||
// 自动开始填表
|
||||
const fillResult = await backendApi.fillTemplate(
|
||||
result.template_id,
|
||||
result.fields || [],
|
||||
[], // 使用 source_file_paths 而非 source_doc_ids
|
||||
result.source_file_paths || [],
|
||||
'请从以下文档中提取相关信息填写表格'
|
||||
// 使用 source_doc_ids 进行填表
|
||||
const fillResult = await backendApi.fillTemplate(
|
||||
result.template_id || 'temp',
|
||||
result.fields || [],
|
||||
sourceDocIds,
|
||||
[],
|
||||
'请从以下文档中提取相关信息填写表格'
|
||||
);
|
||||
|
||||
setFilledResult(fillResult);
|
||||
setStep('preview');
|
||||
toast.success('表格填写完成');
|
||||
}
|
||||
} else {
|
||||
// 使用联合上传API
|
||||
const result = await backendApi.uploadTemplateAndSources(
|
||||
templateFile,
|
||||
sourceFiles.map(sf => sf.file)
|
||||
);
|
||||
|
||||
setFilledResult(fillResult);
|
||||
setStep('preview');
|
||||
toast.success('表格填写完成');
|
||||
if (result.success) {
|
||||
setTemplateFields(result.fields || []);
|
||||
setTemplateId(result.template_id);
|
||||
setSourceFilePaths(result.source_file_paths || []);
|
||||
toast.success('文档上传成功,开始智能填表');
|
||||
setStep('filling');
|
||||
|
||||
// 自动开始填表
|
||||
const fillResult = await backendApi.fillTemplate(
|
||||
result.template_id,
|
||||
result.fields || [],
|
||||
[],
|
||||
result.source_file_paths || [],
|
||||
'请从以下文档中提取相关信息填写表格'
|
||||
);
|
||||
|
||||
setFilledResult(fillResult);
|
||||
setStep('preview');
|
||||
toast.success('表格填写完成');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('处理失败: ' + (err.message || '未知错误'));
|
||||
@@ -264,47 +346,131 @@ const TemplateFill: React.FC = () => {
|
||||
源文档
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
上传包含数据的源文档(支持多选),可同时上传多个文件
|
||||
选择包含数据的源文档作为填表依据
|
||||
</CardDescription>
|
||||
{/* Source Mode Tabs */}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
variant={sourceMode === 'upload' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSourceMode('upload')}
|
||||
>
|
||||
<Upload size={14} className="mr-1" />
|
||||
上传文件
|
||||
</Button>
|
||||
<Button
|
||||
variant={sourceMode === 'select' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSourceMode('select')}
|
||||
>
|
||||
<Files size={14} className="mr-1" />
|
||||
从文档中心选择
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
{...getSourceProps()}
|
||||
className={cn(
|
||||
"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]",
|
||||
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>
|
||||
|
||||
{/* Selected Source Files */}
|
||||
{sourceFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{sourceFiles.map((sf, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3 p-3 bg-muted/50 rounded-xl">
|
||||
{getFileIcon(sf.file.name)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{sf.file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(sf.file.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeSourceFile(idx)}>
|
||||
<Trash2 size={14} className="text-red-500" />
|
||||
</Button>
|
||||
{sourceMode === 'upload' ? (
|
||||
<>
|
||||
<div
|
||||
{...getSourceProps()}
|
||||
className={cn(
|
||||
"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]",
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
<p className="font-medium">
|
||||
{isSourceDragActive ? '释放以上传' : '点击或拖拽上传源文档'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持 .xlsx .xls .docx .md .txt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Selected Source Files */}
|
||||
{sourceFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{sourceFiles.map((sf, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3 p-3 bg-muted/50 rounded-xl">
|
||||
{getFileIcon(sf.file.name)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{sf.file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(sf.file.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeSourceFile(idx)}>
|
||||
<Trash2 size={14} className="text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Uploaded Documents Selection */}
|
||||
{docsLoading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map(i => (
|
||||
<Skeleton key={i} className="h-16 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : uploadedDocuments.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{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 className="text-center py-8 text-muted-foreground">
|
||||
<Files size={32} className="mx-auto mb-2 opacity-30" />
|
||||
<p className="text-sm">暂无可用的已上传文档</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user