新增合同上传附件追加
This commit is contained in:
@@ -136,6 +136,90 @@ export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
|||||||
* @param jwtToken JWT token
|
* @param jwtToken JWT token
|
||||||
* @returns 上传结果
|
* @returns 上传结果
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 合同文档追加附件并合并
|
||||||
|
* @param documentId 合同文档ID
|
||||||
|
* @param files 附件文件列表
|
||||||
|
* @param mergeMode 合并模式:'overwrite'(覆盖原文档)或 'new'(新建文档记录)
|
||||||
|
* @param isReprocess 是否触发重新处理
|
||||||
|
* @param remark 备注
|
||||||
|
* @param jwtToken JWT token
|
||||||
|
* @returns 上传结果
|
||||||
|
*/
|
||||||
|
export async function appendContractAttachments(
|
||||||
|
documentId: number,
|
||||||
|
files: File[],
|
||||||
|
mergeMode: 'overwrite' | 'new' = 'overwrite',
|
||||||
|
isReprocess: boolean = true,
|
||||||
|
remark?: string,
|
||||||
|
jwtToken?: string
|
||||||
|
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||||
|
try {
|
||||||
|
console.log('【合同附件追加】开始追加附件:', { documentId, fileCount: files.length, mergeMode });
|
||||||
|
|
||||||
|
// 创建FormData对象
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 添加多个文件
|
||||||
|
files.forEach(file => {
|
||||||
|
formData.append('files', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加其他参数
|
||||||
|
formData.append('merge_mode', mergeMode);
|
||||||
|
formData.append('is_reprocess', isReprocess.toString());
|
||||||
|
if (remark) {
|
||||||
|
formData.append('remark', remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
const uploadUrl = `${UPLOAD_URL}/contracts/${documentId}/append_attachments`;
|
||||||
|
console.log('【合同附件追加】准备发送请求到服务器:', uploadUrl);
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (jwtToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('【合同附件追加】服务器响应状态:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('【合同附件追加】服务器返回错误:', errorText);
|
||||||
|
return {
|
||||||
|
error: `服务器错误: ${response.status} ${response.statusText}`,
|
||||||
|
status: response.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('【合同附件追加】服务器返回结果:', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { data: result.result };
|
||||||
|
} else {
|
||||||
|
return { error: result.error || '附件追加失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('【合同附件追加】上传过程中发生错误:', error);
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : '附件追加过程中发生未知错误'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadDocumentToServer(
|
export async function uploadDocumentToServer(
|
||||||
binaryData: ArrayBuffer,
|
binaryData: ArrayBuffer,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
|
|||||||
+15
-5
@@ -30,17 +30,17 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
// 验证用户登录状态
|
// 验证用户登录状态
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const { isAuthenticated, userRole } = await getUserSession(request);
|
const { isAuthenticated, userRole, userInfo } = await getUserSession(request);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
return Response.json({ userRole });
|
return Response.json({ userRole, userInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { userRole } = useLoaderData<typeof loader>();
|
const { userRole, userInfo } = useLoaderData<typeof loader>();
|
||||||
const [currentDateTime, setCurrentDateTime] = useState({
|
const [currentDateTime, setCurrentDateTime] = useState({
|
||||||
date: '',
|
date: '',
|
||||||
time: ''
|
time: ''
|
||||||
@@ -133,8 +133,18 @@ export default function Index() {
|
|||||||
<div className="user-info">
|
<div className="user-info">
|
||||||
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
|
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
|
||||||
<div className="user">
|
<div className="user">
|
||||||
<img src="/avatar.png" alt="用户头像" className="avatar" />
|
{(() => {
|
||||||
<span className="username">{userRole === 'developer' ? '系统管理员' : '普通用户'}</span>
|
const displayName = (userInfo?.nick_name || (userInfo as { nickname?: string })?.nickname || (userInfo as { name?: string })?.name || '') as string;
|
||||||
|
const lastChar = displayName ? displayName.charAt(displayName.length - 1) : '用';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
|
||||||
|
<span>{lastChar}</span>
|
||||||
|
</div>
|
||||||
|
<span className="username ml-2">{displayName || '未知用户'}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="logout-button"
|
className="logout-button"
|
||||||
|
|||||||
+10
-2
@@ -124,9 +124,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||||
console.log("前端JWT已生成");
|
console.log("前端JWT已生成");
|
||||||
|
|
||||||
// 更新userInfo以包含数据库ID和JWT信息
|
// 更新userInfo以包含数据库ID、JWT,并用数据库标准字段覆盖关键属性,确保 nick_name 等存在
|
||||||
const enhancedUserInfo = {
|
const enhancedUserInfo = {
|
||||||
...userInfo.data,
|
...userInfo.data, // 保留OAuth返回的原字段(包含 nickname 等)
|
||||||
|
username: savedUserData.username,
|
||||||
|
nick_name: savedUserData.nick_name,
|
||||||
|
phone_number: savedUserData.phone_number,
|
||||||
|
email: savedUserData.email,
|
||||||
|
ou_id: savedUserData.ou_id,
|
||||||
|
ou_name: savedUserData.ou_name,
|
||||||
|
status: savedUserData.status,
|
||||||
|
is_leader: savedUserData.is_leader,
|
||||||
user_id: savedUserData.id,
|
user_id: savedUserData.id,
|
||||||
user_role: userRole,
|
user_role: userRole,
|
||||||
frontend_jwt: frontendJWT
|
frontend_jwt: frontendJWT
|
||||||
|
|||||||
+255
-10
@@ -16,6 +16,7 @@ import {
|
|||||||
getDocumentsStatus,
|
getDocumentsStatus,
|
||||||
uploadFileToBinary,
|
uploadFileToBinary,
|
||||||
uploadDocumentToServer,
|
uploadDocumentToServer,
|
||||||
|
appendContractAttachments,
|
||||||
type Document,
|
type Document,
|
||||||
type DocumentType,
|
type DocumentType,
|
||||||
type FileUploadResponse,
|
type FileUploadResponse,
|
||||||
@@ -304,6 +305,14 @@ export default function FilesUpload() {
|
|||||||
const [isContractType, setIsContractType] = useState<boolean>(false);
|
const [isContractType, setIsContractType] = useState<boolean>(false);
|
||||||
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
|
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
|
||||||
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
|
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
|
// 附件追加状态
|
||||||
|
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||||||
|
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
||||||
|
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||||||
|
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
||||||
|
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||||||
|
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
||||||
@@ -768,6 +777,93 @@ export default function FilesUpload() {
|
|||||||
console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error);
|
console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理附件追加文件选择
|
||||||
|
const handleAttachmentFilesSelected = (files: FileList) => {
|
||||||
|
try {
|
||||||
|
console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length);
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||||||
|
const validFiles: File[] = [];
|
||||||
|
let hasInvalidFiles = false;
|
||||||
|
|
||||||
|
Array.from(files).forEach(file => {
|
||||||
|
const fileName = file.name.toLowerCase();
|
||||||
|
const isValidType =
|
||||||
|
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||||
|
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||||
|
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||||
|
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||||
|
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||||||
|
|
||||||
|
if (isValidType) {
|
||||||
|
validFiles.push(file);
|
||||||
|
} else {
|
||||||
|
hasInvalidFiles = true;
|
||||||
|
console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInvalidFiles) {
|
||||||
|
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||||||
|
title: '文件类型错误',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
setAttachmentFiles(validFiles);
|
||||||
|
console.log('【附件追加】有效文件数量:', validFiles.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('【附件追加】处理文件选择时发生错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理附件追加上传
|
||||||
|
const handleAttachmentUpload = async () => {
|
||||||
|
if (!selectedDocumentId || attachmentFiles.length === 0) {
|
||||||
|
toastService.error('请选择文档和附件文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setAttachmentUploading(true);
|
||||||
|
|
||||||
|
const result = await appendContractAttachments(
|
||||||
|
selectedDocumentId,
|
||||||
|
attachmentFiles,
|
||||||
|
attachmentMergeMode,
|
||||||
|
true, // isReprocess
|
||||||
|
attachmentRemark || undefined,
|
||||||
|
loaderData.userInfo?.token
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
toastService.success('附件追加成功!');
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
setAttachmentFiles([]);
|
||||||
|
setAttachmentRemark("");
|
||||||
|
setShowAttachmentUpload(false);
|
||||||
|
setSelectedDocumentId(null);
|
||||||
|
|
||||||
|
// 刷新文档列表
|
||||||
|
await filterDocuments(reviewType);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('【附件追加】上传失败:', error);
|
||||||
|
toastService.error(error instanceof Error ? error.message : '附件追加失败');
|
||||||
|
} finally {
|
||||||
|
setAttachmentUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 检查并准备上传
|
// 检查并准备上传
|
||||||
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => {
|
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => {
|
||||||
@@ -1676,17 +1772,32 @@ export default function FilesUpload() {
|
|||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
width: "15%",
|
width: "20%",
|
||||||
render: (_: unknown, record: Document) => (
|
render: (_: unknown, record: Document) => (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
type="default"
|
<Button
|
||||||
size="small"
|
type="default"
|
||||||
disabled={record.status !== DocumentStatus.PROCESSED}
|
size="small"
|
||||||
icon="ri-eye-line"
|
disabled={record.status !== DocumentStatus.PROCESSED}
|
||||||
onClick={() => handleViewFile(record)}
|
icon="ri-eye-line"
|
||||||
>
|
onClick={() => handleViewFile(record)}
|
||||||
查看
|
>
|
||||||
</Button>
|
查看
|
||||||
|
</Button>
|
||||||
|
{record.type_id === 1 && record.status === DocumentStatus.PROCESSED && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="ri-attachment-line"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDocumentId(record.id);
|
||||||
|
setShowAttachmentUpload(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
追加附件
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -2065,6 +2176,140 @@ export default function FilesUpload() {
|
|||||||
emptyText="暂无上传文件"
|
emptyText="暂无上传文件"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 附件追加模态框 */}
|
||||||
|
{showAttachmentUpload && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">追加合同附件</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowAttachmentUpload(false);
|
||||||
|
setSelectedDocumentId(null);
|
||||||
|
setAttachmentFiles([]);
|
||||||
|
setAttachmentRemark("");
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<i className="ri-close-line text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 文档信息 */}
|
||||||
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文件上传区域 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
选择附件文件 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<UploadArea
|
||||||
|
onFilesSelected={handleAttachmentFilesSelected}
|
||||||
|
multiple={true}
|
||||||
|
accept=".pdf,.doc,.docx,.zip,.rar"
|
||||||
|
tipText="支持PDF、Word、ZIP、RAR格式,可多选"
|
||||||
|
mainText="选择附件文件"
|
||||||
|
buttonText="选择文件"
|
||||||
|
icon="ri-attachment-line"
|
||||||
|
/>
|
||||||
|
{attachmentFiles.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-green-600 mb-2">
|
||||||
|
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||||
|
{attachmentFiles.map((file, index) => (
|
||||||
|
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||||||
|
<i className="ri-file-line mr-1"></i>
|
||||||
|
{file.name} ({formatFileSize(file.size)})
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 合并模式选择 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
合并模式
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="mergeMode"
|
||||||
|
value="overwrite"
|
||||||
|
checked={attachmentMergeMode === 'overwrite'}
|
||||||
|
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">覆盖原文档(推荐)</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="mergeMode"
|
||||||
|
value="new"
|
||||||
|
checked={attachmentMergeMode === 'new'}
|
||||||
|
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">新建文档记录</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 备注 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
备注(可选)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={attachmentRemark}
|
||||||
|
onChange={(e) => setAttachmentRemark(e.target.value)}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入备注信息..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAttachmentUpload(false);
|
||||||
|
setSelectedDocumentId(null);
|
||||||
|
setAttachmentFiles([]);
|
||||||
|
setAttachmentRemark("");
|
||||||
|
}}
|
||||||
|
disabled={attachmentUploading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleAttachmentUpload}
|
||||||
|
disabled={attachmentFiles.length === 0 || attachmentUploading}
|
||||||
|
icon={attachmentUploading ? "ri-loader-4-line" : "ri-upload-cloud-line"}
|
||||||
|
>
|
||||||
|
{attachmentUploading ? '上传中...' : '开始追加'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user