新增从文档中心选择源文档功能及删除功能
智能填表模块新增"从文档中心选择"模式,支持选择已上传的文档作为数据源, 同时支持从列表中删除文档。两种模式通过Tab切换。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ interface TemplateFillState {
|
|||||||
templateFields: TemplateField[];
|
templateFields: TemplateField[];
|
||||||
sourceFiles: SourceFile[];
|
sourceFiles: SourceFile[];
|
||||||
sourceFilePaths: string[];
|
sourceFilePaths: string[];
|
||||||
|
sourceDocIds: string[];
|
||||||
templateId: string;
|
templateId: string;
|
||||||
filledResult: any;
|
filledResult: any;
|
||||||
setStep: (step: Step) => void;
|
setStep: (step: Step) => void;
|
||||||
@@ -30,6 +31,9 @@ interface TemplateFillState {
|
|||||||
addSourceFiles: (files: SourceFile[]) => void;
|
addSourceFiles: (files: SourceFile[]) => void;
|
||||||
removeSourceFile: (index: number) => void;
|
removeSourceFile: (index: number) => void;
|
||||||
setSourceFilePaths: (paths: string[]) => void;
|
setSourceFilePaths: (paths: string[]) => void;
|
||||||
|
setSourceDocIds: (ids: string[]) => void;
|
||||||
|
addSourceDocId: (id: string) => void;
|
||||||
|
removeSourceDocId: (id: string) => void;
|
||||||
setTemplateId: (id: string) => void;
|
setTemplateId: (id: string) => void;
|
||||||
setFilledResult: (result: any) => void;
|
setFilledResult: (result: any) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
@@ -41,6 +45,7 @@ const initialState = {
|
|||||||
templateFields: [],
|
templateFields: [],
|
||||||
sourceFiles: [],
|
sourceFiles: [],
|
||||||
sourceFilePaths: [],
|
sourceFilePaths: [],
|
||||||
|
sourceDocIds: [],
|
||||||
templateId: '',
|
templateId: '',
|
||||||
filledResult: null,
|
filledResult: null,
|
||||||
setStep: () => {},
|
setStep: () => {},
|
||||||
@@ -50,6 +55,9 @@ const initialState = {
|
|||||||
addSourceFiles: () => {},
|
addSourceFiles: () => {},
|
||||||
removeSourceFile: () => {},
|
removeSourceFile: () => {},
|
||||||
setSourceFilePaths: () => {},
|
setSourceFilePaths: () => {},
|
||||||
|
setSourceDocIds: () => {},
|
||||||
|
addSourceDocId: () => {},
|
||||||
|
removeSourceDocId: () => {},
|
||||||
setTemplateId: () => {},
|
setTemplateId: () => {},
|
||||||
setFilledResult: () => {},
|
setFilledResult: () => {},
|
||||||
reset: () => {},
|
reset: () => {},
|
||||||
@@ -63,6 +71,7 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
|||||||
const [templateFields, setTemplateFields] = useState<TemplateField[]>([]);
|
const [templateFields, setTemplateFields] = useState<TemplateField[]>([]);
|
||||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||||
const [sourceFilePaths, setSourceFilePaths] = useState<string[]>([]);
|
const [sourceFilePaths, setSourceFilePaths] = useState<string[]>([]);
|
||||||
|
const [sourceDocIds, setSourceDocIds] = useState<string[]>([]);
|
||||||
const [templateId, setTemplateId] = useState<string>('');
|
const [templateId, setTemplateId] = useState<string>('');
|
||||||
const [filledResult, setFilledResult] = useState<any>(null);
|
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));
|
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 = () => {
|
const reset = () => {
|
||||||
setStep('upload');
|
setStep('upload');
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
setTemplateFields([]);
|
setTemplateFields([]);
|
||||||
setSourceFiles([]);
|
setSourceFiles([]);
|
||||||
setSourceFilePaths([]);
|
setSourceFilePaths([]);
|
||||||
|
setSourceDocIds([]);
|
||||||
setTemplateId('');
|
setTemplateId('');
|
||||||
setFilledResult(null);
|
setFilledResult(null);
|
||||||
};
|
};
|
||||||
@@ -92,6 +110,7 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
|||||||
templateFields,
|
templateFields,
|
||||||
sourceFiles,
|
sourceFiles,
|
||||||
sourceFilePaths,
|
sourceFilePaths,
|
||||||
|
sourceDocIds,
|
||||||
templateId,
|
templateId,
|
||||||
filledResult,
|
filledResult,
|
||||||
setStep,
|
setStep,
|
||||||
@@ -101,6 +120,9 @@ export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ childr
|
|||||||
addSourceFiles,
|
addSourceFiles,
|
||||||
removeSourceFile,
|
removeSourceFile,
|
||||||
setSourceFilePaths,
|
setSourceFilePaths,
|
||||||
|
setSourceDocIds,
|
||||||
|
addSourceDocId,
|
||||||
|
removeSourceDocId,
|
||||||
setTemplateId,
|
setTemplateId,
|
||||||
setFilledResult,
|
setFilledResult,
|
||||||
reset,
|
reset,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const TemplateFill: React.FC = () => {
|
|||||||
templateFields, setTemplateFields,
|
templateFields, setTemplateFields,
|
||||||
sourceFiles, setSourceFiles, addSourceFiles, removeSourceFile,
|
sourceFiles, setSourceFiles, addSourceFiles, removeSourceFile,
|
||||||
sourceFilePaths, setSourceFilePaths,
|
sourceFilePaths, setSourceFilePaths,
|
||||||
|
sourceDocIds, setSourceDocIds, addSourceDocId, removeSourceDocId,
|
||||||
templateId, setTemplateId,
|
templateId, setTemplateId,
|
||||||
filledResult, setFilledResult,
|
filledResult, setFilledResult,
|
||||||
reset
|
reset
|
||||||
@@ -68,6 +69,9 @@ const TemplateFill: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null);
|
const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null);
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
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[]) => {
|
const onTemplateDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
@@ -109,40 +113,118 @@ const TemplateFill: React.FC = () => {
|
|||||||
multiple: true
|
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 () => {
|
const handleJointUploadAndFill = async () => {
|
||||||
if (!templateFile) {
|
if (!templateFile) {
|
||||||
toast.error('请先上传模板文件');
|
toast.error('请先上传模板文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否选择了数据源
|
||||||
|
if (sourceMode === 'upload' && sourceFiles.length === 0) {
|
||||||
|
toast.error('请上传源文档或从已上传文档中选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sourceMode === 'select' && sourceDocIds.length === 0) {
|
||||||
|
toast.error('请选择源文档');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用联合上传API
|
if (sourceMode === 'select') {
|
||||||
const result = await backendApi.uploadTemplateAndSources(
|
// 使用已上传文档作为数据源
|
||||||
templateFile,
|
const result = await backendApi.uploadTemplate(templateFile);
|
||||||
sourceFiles.map(sf => sf.file)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setTemplateFields(result.fields || []);
|
setTemplateFields(result.fields || []);
|
||||||
setTemplateId(result.template_id);
|
setTemplateId(result.template_id || 'temp');
|
||||||
setSourceFilePaths(result.source_file_paths || []);
|
toast.success('开始智能填表');
|
||||||
toast.success('文档上传成功,开始智能填表');
|
setStep('filling');
|
||||||
setStep('filling');
|
|
||||||
|
|
||||||
// 自动开始填表
|
// 使用 source_doc_ids 进行填表
|
||||||
const fillResult = await backendApi.fillTemplate(
|
const fillResult = await backendApi.fillTemplate(
|
||||||
result.template_id,
|
result.template_id || 'temp',
|
||||||
result.fields || [],
|
result.fields || [],
|
||||||
[], // 使用 source_file_paths 而非 source_doc_ids
|
sourceDocIds,
|
||||||
result.source_file_paths || [],
|
[],
|
||||||
'请从以下文档中提取相关信息填写表格'
|
'请从以下文档中提取相关信息填写表格'
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilledResult(fillResult);
|
||||||
|
setStep('preview');
|
||||||
|
toast.success('表格填写完成');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用联合上传API
|
||||||
|
const result = await backendApi.uploadTemplateAndSources(
|
||||||
|
templateFile,
|
||||||
|
sourceFiles.map(sf => sf.file)
|
||||||
);
|
);
|
||||||
|
|
||||||
setFilledResult(fillResult);
|
if (result.success) {
|
||||||
setStep('preview');
|
setTemplateFields(result.fields || []);
|
||||||
toast.success('表格填写完成');
|
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) {
|
} catch (err: any) {
|
||||||
toast.error('处理失败: ' + (err.message || '未知错误'));
|
toast.error('处理失败: ' + (err.message || '未知错误'));
|
||||||
@@ -264,47 +346,131 @@ const TemplateFill: React.FC = () => {
|
|||||||
源文档
|
源文档
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
上传包含数据的源文档(支持多选),可同时上传多个文件
|
选择包含数据的源文档作为填表依据
|
||||||
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div
|
{sourceMode === 'upload' ? (
|
||||||
{...getSourceProps()}
|
<>
|
||||||
className={cn(
|
<div
|
||||||
"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]",
|
{...getSourceProps()}
|
||||||
isSourceDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/20 hover:border-primary/50 hover:bg-primary/5"
|
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} />}
|
<input {...getSourceInputProps()} />
|
||||||
</div>
|
<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">
|
||||||
<p className="font-medium">
|
{loading ? <Loader2 className="animate-spin" size={28} /> : <Upload size={28} />}
|
||||||
{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>
|
||||||
))}
|
<p className="font-medium">
|
||||||
</div>
|
{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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user