添加AI生成表头功能并重构前端状态管理
- 后端:实现AI生成表头逻辑,当模板为空或字段为自动生成时调用AI分析并生成合适字段 - 后端:添加_is_auto_generated_field方法识别自动生成的无效表头字段 - 后端:修改_get_template_fields_from_excel方法支持文件类型参数 - 前端:创建TemplateFillContext提供全局状态管理 - 前端:将TemplateFill页面状态迁移到Context中统一管理 - 前端:移除页面内重复的状态定义和方法实现
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { TemplateFillProvider } from '@/context/TemplateFillContext';
|
||||
import { router } from '@/routes';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
<TemplateFillProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
</TemplateFillProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
114
frontend/src/context/TemplateFillContext.tsx
Normal file
114
frontend/src/context/TemplateFillContext.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
type SourceFile = {
|
||||
file: File;
|
||||
preview?: string;
|
||||
};
|
||||
|
||||
type TemplateField = {
|
||||
cell: string;
|
||||
name: string;
|
||||
field_type: string;
|
||||
required: boolean;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
type Step = 'upload' | 'filling' | 'preview';
|
||||
|
||||
interface TemplateFillState {
|
||||
step: Step;
|
||||
templateFile: File | null;
|
||||
templateFields: TemplateField[];
|
||||
sourceFiles: SourceFile[];
|
||||
sourceFilePaths: string[];
|
||||
templateId: string;
|
||||
filledResult: any;
|
||||
setStep: (step: Step) => void;
|
||||
setTemplateFile: (file: File | null) => void;
|
||||
setTemplateFields: (fields: TemplateField[]) => void;
|
||||
setSourceFiles: (files: SourceFile[]) => void;
|
||||
addSourceFiles: (files: SourceFile[]) => void;
|
||||
removeSourceFile: (index: number) => void;
|
||||
setSourceFilePaths: (paths: string[]) => void;
|
||||
setTemplateId: (id: string) => void;
|
||||
setFilledResult: (result: any) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
step: 'upload' as Step,
|
||||
templateFile: null,
|
||||
templateFields: [],
|
||||
sourceFiles: [],
|
||||
sourceFilePaths: [],
|
||||
templateId: '',
|
||||
filledResult: null,
|
||||
setStep: () => {},
|
||||
setTemplateFile: () => {},
|
||||
setTemplateFields: () => {},
|
||||
setSourceFiles: () => {},
|
||||
addSourceFiles: () => {},
|
||||
removeSourceFile: () => {},
|
||||
setSourceFilePaths: () => {},
|
||||
setTemplateId: () => {},
|
||||
setFilledResult: () => {},
|
||||
reset: () => {},
|
||||
};
|
||||
|
||||
const TemplateFillContext = createContext<TemplateFillState>(initialState);
|
||||
|
||||
export const TemplateFillProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [step, setStep] = useState<Step>('upload');
|
||||
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||||
const [templateFields, setTemplateFields] = useState<TemplateField[]>([]);
|
||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||
const [sourceFilePaths, setSourceFilePaths] = useState<string[]>([]);
|
||||
const [templateId, setTemplateId] = useState<string>('');
|
||||
const [filledResult, setFilledResult] = useState<any>(null);
|
||||
|
||||
const addSourceFiles = (files: SourceFile[]) => {
|
||||
setSourceFiles(prev => [...prev, ...files]);
|
||||
};
|
||||
|
||||
const removeSourceFile = (index: number) => {
|
||||
setSourceFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setStep('upload');
|
||||
setTemplateFile(null);
|
||||
setTemplateFields([]);
|
||||
setSourceFiles([]);
|
||||
setSourceFilePaths([]);
|
||||
setTemplateId('');
|
||||
setFilledResult(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<TemplateFillContext.Provider
|
||||
value={{
|
||||
step,
|
||||
templateFile,
|
||||
templateFields,
|
||||
sourceFiles,
|
||||
sourceFilePaths,
|
||||
templateId,
|
||||
filledResult,
|
||||
setStep,
|
||||
setTemplateFile,
|
||||
setTemplateFields,
|
||||
setSourceFiles,
|
||||
addSourceFiles,
|
||||
removeSourceFile,
|
||||
setSourceFilePaths,
|
||||
setTemplateId,
|
||||
setFilledResult,
|
||||
reset,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TemplateFillContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTemplateFill = () => useContext(TemplateFillContext);
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useTemplateFill } from '@/context/TemplateFillContext';
|
||||
|
||||
type DocumentItem = {
|
||||
doc_id: string;
|
||||
@@ -52,29 +53,19 @@ type DocumentItem = {
|
||||
};
|
||||
};
|
||||
|
||||
type SourceFile = {
|
||||
file: File;
|
||||
preview?: string;
|
||||
};
|
||||
|
||||
type TemplateField = {
|
||||
cell: string;
|
||||
name: string;
|
||||
field_type: string;
|
||||
required: boolean;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
const TemplateFill: React.FC = () => {
|
||||
const [step, setStep] = useState<'upload' | 'filling' | 'preview'>('upload');
|
||||
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||||
const [templateFields, setTemplateFields] = useState<TemplateField[]>([]);
|
||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||
const [sourceFilePaths, setSourceFilePaths] = useState<string[]>([]);
|
||||
const [templateId, setTemplateId] = useState<string>('');
|
||||
const {
|
||||
step, setStep,
|
||||
templateFile, setTemplateFile,
|
||||
templateFields, setTemplateFields,
|
||||
sourceFiles, setSourceFiles, addSourceFiles, removeSourceFile,
|
||||
sourceFilePaths, setSourceFilePaths,
|
||||
templateId, setTemplateId,
|
||||
filledResult, setFilledResult,
|
||||
reset
|
||||
} = useTemplateFill();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filling, setFilling] = useState(false);
|
||||
const [filledResult, setFilledResult] = useState<any>(null);
|
||||
const [previewDoc, setPreviewDoc] = useState<{ name: string; content: string } | null>(null);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
@@ -103,8 +94,8 @@ const TemplateFill: React.FC = () => {
|
||||
file: f,
|
||||
preview: f.type.startsWith('text/') || f.name.endsWith('.md') ? undefined : undefined
|
||||
}));
|
||||
setSourceFiles(prev => [...prev, ...newFiles]);
|
||||
}, []);
|
||||
addSourceFiles(newFiles);
|
||||
}, [addSourceFiles]);
|
||||
|
||||
const { getRootProps: getSourceProps, getInputProps: getSourceInputProps, isDragActive: isSourceDragActive } = useDropzone({
|
||||
onDrop: onSourceDrop,
|
||||
@@ -118,10 +109,6 @@ const TemplateFill: React.FC = () => {
|
||||
multiple: true
|
||||
});
|
||||
|
||||
const removeSourceFile = (index: number) => {
|
||||
setSourceFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleJointUploadAndFill = async () => {
|
||||
if (!templateFile) {
|
||||
toast.error('请先上传模板文件');
|
||||
@@ -164,40 +151,6 @@ const TemplateFill: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 传统方式:先上传源文档再填表(兼容已有文档库的场景)
|
||||
const handleFillWithExistingDocs = async (selectedDocIds: string[]) => {
|
||||
if (!templateFile || selectedDocIds.length === 0) {
|
||||
toast.error('请选择数据源文档');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setStep('filling');
|
||||
|
||||
try {
|
||||
// 先上传模板获取template_id
|
||||
const uploadResult = await backendApi.uploadTemplate(templateFile);
|
||||
|
||||
const fillResult = await backendApi.fillTemplate(
|
||||
uploadResult.template_id,
|
||||
uploadResult.fields || [],
|
||||
selectedDocIds,
|
||||
[],
|
||||
'请从以下文档中提取相关信息填写表格'
|
||||
);
|
||||
|
||||
setTemplateFields(uploadResult.fields || []);
|
||||
setTemplateId(uploadResult.template_id);
|
||||
setFilledResult(fillResult);
|
||||
setStep('preview');
|
||||
toast.success('表格填写完成');
|
||||
} catch (err: any) {
|
||||
toast.error('填表失败: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!templateFile || !filledResult) return;
|
||||
|
||||
@@ -219,16 +172,6 @@ const TemplateFill: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetFlow = () => {
|
||||
setStep('upload');
|
||||
setTemplateFile(null);
|
||||
setTemplateFields([]);
|
||||
setSourceFiles([]);
|
||||
setSourceFilePaths([]);
|
||||
setTemplateId('');
|
||||
setFilledResult(null);
|
||||
};
|
||||
|
||||
const getFileIcon = (filename: string) => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
if (['xlsx', 'xls'].includes(ext || '')) {
|
||||
@@ -253,7 +196,7 @@ const TemplateFill: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
{step !== 'upload' && (
|
||||
<Button variant="outline" className="rounded-xl gap-2" onClick={resetFlow}>
|
||||
<Button variant="outline" className="rounded-xl gap-2" onClick={reset}>
|
||||
<RefreshCcw size={18} />
|
||||
<span>重新开始</span>
|
||||
</Button>
|
||||
@@ -451,7 +394,7 @@ const TemplateFill: React.FC = () => {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-center gap-4 mt-6">
|
||||
<Button variant="outline" className="rounded-xl gap-2" onClick={resetFlow}>
|
||||
<Button variant="outline" className="rounded-xl gap-2" onClick={reset}>
|
||||
<RefreshCcw size={18} />
|
||||
<span>继续填表</span>
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user