更新附件追加功能,优化文件选择和验证逻辑,支持多种文件格式,调整用户界面以提升操作体验。

This commit is contained in:
2025-09-22 11:17:40 +08:00
parent acb717c342
commit 1ea9fb205c
2 changed files with 254 additions and 12 deletions
+3 -3
View File
@@ -1064,11 +1064,11 @@ export default function DocumentsIndex() {
<i className="ri-download-line"></i> <i className="ri-download-line"></i>
</button> </button>
{record.type === 1 && record.fileStatus === 'Processed' && ( {record.type === '1' && record.fileStatus === 'Processed' && (
<> <>
<button <button
type="button" type="button"
className="text-xs px-2 py-1 h-7 mr-1 bg-primary text-white hover:bg-primary-dark rounded" className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
onClick={() => { onClick={() => {
setSelectedDocumentId(record.id); setSelectedDocumentId(record.id);
setShowAttachmentUpload(true); setShowAttachmentUpload(true);
@@ -1079,7 +1079,7 @@ export default function DocumentsIndex() {
</button> </button>
<button <button
type="button" type="button"
className="text-xs px-2 py-1 h-7 mr-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded" className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => { onClick={() => {
setSelectedDocumentId(record.id); setSelectedDocumentId(record.id);
setShowTemplateUpload(true); setShowTemplateUpload(true);
+251 -9
View File
@@ -21,6 +21,8 @@ import { getDocumentTypes } from "~/api/document-types/document-types";
import { toastService } from "~/components/ui/Toast"; import { toastService } from "~/components/ui/Toast";
// 导入axios下载文件方法 // 导入axios下载文件方法
import { downloadFile } from "~/api/axios-client"; import { downloadFile } from "~/api/axios-client";
import { appendContractAttachments } from "~/api/files/files-upload";
import { messageService } from "~/components/ui/MessageModal";
export const links = () => [ export const links = () => [
{ rel: "stylesheet", href: rulesFilesStyles }, { rel: "stylesheet", href: rulesFilesStyles },
@@ -60,7 +62,7 @@ export const REVIEW_STATUS_LABELS: Record<string, string> = {
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
// 获取用户会话信息 // 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
// 获取分页参数 // 获取分页参数
const url = new URL(request.url); const url = new URL(request.url);
@@ -80,6 +82,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
currentPage, currentPage,
pageSize, pageSize,
userInfo, // 传递用户信息到客户端 userInfo, // 传递用户信息到客户端
frontendJWT,
initialLoad: true initialLoad: true
}); });
} catch (error) { } catch (error) {
@@ -90,7 +93,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
export default function RulesFiles() { export default function RulesFiles() {
const navigate = useNavigate(); const navigate = useNavigate();
const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, result, message } = useLoaderData<typeof loader>(); const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, frontendJWT, result, message } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const dateFrom = searchParams.get('dateFrom') || ''; const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || ''; const dateTo = searchParams.get('dateTo') || '';
@@ -102,6 +105,14 @@ export default function RulesFiles() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [reviewType, setReviewType] = useState<string | null>(null); const [reviewType, setReviewType] = useState<string | null>(null);
// 附件追加相关状态
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
const [selectedDocumentId, setSelectedDocumentId] = useState<string | 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);
// 保存/恢复 查询参数 的 sessionStorage key // 保存/恢复 查询参数 的 sessionStorage key
const SEARCH_PARAMS_STORAGE_KEY = 'rulesFiles.searchParams'; const SEARCH_PARAMS_STORAGE_KEY = 'rulesFiles.searchParams';
@@ -446,6 +457,87 @@ export default function RulesFiles() {
} }
setSearchParams(newParams); setSearchParams(newParams);
}; };
// 辅助:格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
// 选择附件文件
const handleAttachmentFilesSelected = (files: FileList) => {
try {
if (files.length > 0) {
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;
}
});
if (hasInvalidFiles) {
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
title: '文件类型错误',
confirmText: '确定',
cancelText: '',
});
}
if (validFiles.length > 0) {
setAttachmentFiles(validFiles);
}
}
} catch (error) {
console.error('【附件追加】处理文件选择时发生错误:', error);
}
};
// 执行附件追加
const handleAttachmentUpload = async () => {
if (!selectedDocumentId || attachmentFiles.length === 0) {
toastService.error('请选择文档和附件文件');
return;
}
try {
setAttachmentUploading(true);
const docId = parseInt(selectedDocumentId, 10);
const result = await appendContractAttachments(
docId,
attachmentFiles,
attachmentMergeMode,
true,
attachmentRemark || undefined,
frontendJWT as string | undefined
);
if (result.error) {
throw new Error(result.error);
}
toastService.success('附件追加成功!');
// 重置并关闭
setAttachmentFiles([]);
setAttachmentRemark("");
setShowAttachmentUpload(false);
setSelectedDocumentId(null);
// 刷新列表
fetchData(Object.fromEntries(searchParams.entries()));
} catch (error) {
console.error('【附件追加】上传失败:', error);
toastService.error(error instanceof Error ? error.message : '附件追加失败');
} finally {
setAttachmentUploading(false);
}
};
// 文件类型选项 // 文件类型选项
const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({ const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({
@@ -560,25 +652,42 @@ export default function RulesFiles() {
{ {
title: "操作", title: "操作",
key: "operation", key: "operation",
width: "14%", width: "20%",
render: (_: unknown, file: ReviewFileUI) => ( render: (_: unknown, file: ReviewFileUI) => (
<> <div className="flex flex-wrap gap-1">
<Button <Button
type="default" type="default"
size="small" size="small"
icon="ri-eye-line" icon="ri-eye-line"
// to={`/reviews?id=${file.id}`}
onClick={() => handleReviewFileClick(file.id, file.auditStatus)} onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
disabled={file.status !== 'Processed'} disabled={file.status !== 'Processed'}
className="mr-2" className="mr-1"
> >
</Button> </Button>
{reviewType === 'contract' && file.status === 'Processed' && (
<Button type="default" size="small" icon="ri-download-2-line" className="mt-1" onClick={() => handleDownload(file.path)}> <button
type="button"
className="text-xs px-2 py-1 h-7 mr-1 bg-primary text-white hover:bg-primary-dark rounded"
onClick={() => {
setSelectedDocumentId(file.id);
setShowAttachmentUpload(true);
}}
>
<i className="ri-attachment-line"></i>
</button>
)}
<Button
type="default"
size="small"
icon="ri-download-2-line"
className="mt-1"
onClick={() => handleDownload(file.path)}
>
</Button> </Button>
</> </div>
) )
} }
]; ];
@@ -705,6 +814,139 @@ export default function RulesFiles() {
/> />
)} )}
</div> </div>
{/* 附件追加模态框 */}
{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">
PDFWordZIPRAR格式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>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.zip,.rar"
onChange={(e) => e.target.files && handleAttachmentFilesSelected(e.target.files)}
className="hidden"
id="attachment-file-input"
/>
<label htmlFor="attachment-file-input" className="cursor-pointer">
<i className="ri-attachment-line text-3xl text-gray-400 mb-2 block"></i>
<p className="text-sm text-gray-600"></p>
<p className="text-xs text-gray-500 mt-1">PDFWordZIPRAR格式</p>
</label>
</div>
{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
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
onClick={() => {
setShowAttachmentUpload(false);
setSelectedDocumentId(null);
setAttachmentFiles([]);
setAttachmentRemark("");
}}
disabled={attachmentUploading}
>
</button>
<button
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
onClick={handleAttachmentUpload}
disabled={attachmentFiles.length === 0 || attachmentUploading}
>
{attachmentUploading ? '上传中...' : '开始追加'}
</button>
</div>
</div>
</div>
</div>
)}
</Card> </Card>
</div> </div>
); );