更新附件追加功能,优化文件选择和验证逻辑,支持多种文件格式,调整用户界面以提升操作体验。
This commit is contained in:
@@ -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
@@ -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';
|
||||||
|
|
||||||
@@ -447,6 +458,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}) => ({
|
||||||
value: type.id.toString(),
|
value: type.id.toString(),
|
||||||
@@ -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">
|
||||||
|
支持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>
|
||||||
|
<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">支持PDF、Word、ZIP、RAR格式,可多选</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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user