feat: align frontend document and rule management flows

This commit is contained in:
wren
2026-05-06 09:40:37 +08:00
parent 8a5044b024
commit c54f84382b
41 changed files with 4239 additions and 2903 deletions
+127 -19
View File
@@ -14,6 +14,7 @@ import {
buildUploadErrorDetails,
getTodayDocuments,
getDocumentTypes,
getDocumentSubtypeGroups,
getDocumentsStatus,
uploadFileToBinary,
uploadDocumentToServer,
@@ -22,6 +23,7 @@ import {
checkDocumentDuplicate,
type Document,
type DocumentType,
type DocumentSubtypeGroup,
type UploadErrorDetails,
type UploadResult,
DocumentStatus
@@ -29,7 +31,6 @@ import {
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
import { getQueueStatus, type QueueStatus } from "~/api/queue";
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from "~/constants/contractTypes";
export function links() {
return [
@@ -132,9 +133,11 @@ async function handleFileUpload(
fileName: string,
fileType: string,
documentType: FileType,
groupId: number | null,
priority: Priority,
region: string,
createdBy?: number,
attachments?: File[],
jwtToken?: string,
): Promise<UploadResult> {
const speed = priority === Priority.NORMAL ? "normal" : "urgent";
@@ -144,8 +147,10 @@ async function handleFileUpload(
fileName,
fileType,
Number(documentType),
groupId,
region,
createdBy,
attachments,
true,
speed,
jwtToken,
@@ -331,6 +336,7 @@ export default function FilesUpload() {
// 获取 sessionStorage 中的 documentTypeIds 值
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [documentTypeIds, setDocumentTypeIds] = useState<number[] | null>(null);
const [selectedModuleId, setSelectedModuleId] = useState<number | null>(null);
// 使用 useLoaderData 获取初始数据
const loaderData = useLoaderData<LoaderData>();
@@ -344,7 +350,9 @@ export default function FilesUpload() {
const [documentNumber, setDocumentNumber] = useState<string>("");
const [remark, setRemark] = useState<string>("");
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
const [attributeType, setAttributeType] = useState<string>(DEFAULT_CONTRACT_TYPE);
const [subtypeGroups, setSubtypeGroups] = useState<DocumentSubtypeGroup[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string>("");
const [groupOptionsLoading, setGroupOptionsLoading] = useState(false);
// 合同文件上传状态
// 这些变量暂时未使用,但保留以备将来扩展
@@ -393,6 +401,16 @@ export default function FilesUpload() {
loaderData.documentTypes.find(type => type.id.toString() === fileType) ||
null;
const getSubtypeDisplayName = (group: DocumentSubtypeGroup | null | undefined) =>
group?.displayName || group?.name || "";
const selectedSubtypeGroup = subtypeGroups.find(group => String(group.id) === selectedGroupId) || null;
const hasMultipleSubtypeGroups = subtypeGroups.length > 1;
const singleSubtypeGroup = subtypeGroups.length === 1 ? subtypeGroups[0] : null;
const isSingleDefaultSubtype = !!(singleSubtypeGroup && singleSubtypeGroup.isDefault);
const selectedRootGroupName = selectedSubtypeGroup?.rootGroupName || singleSubtypeGroup?.rootGroupName || "";
const selectedEntryModuleName = selectedSubtypeGroup?.entryModuleName || singleSubtypeGroup?.entryModuleName || "";
const clearUploadErrorDetails = () => {
setUploadErrorDetails(null);
};
@@ -444,6 +462,8 @@ export default function FilesUpload() {
? nextSelectedModuleId
: null;
setSelectedModuleId(normalizedModuleId);
if (cancelled) {
return;
}
@@ -842,6 +862,51 @@ export default function FilesUpload() {
setFileTypeError("上传文件之前请选择文件类型");
}
};
useEffect(() => {
let cancelled = false;
const loadSubtypeGroups = async () => {
if (!fileType) {
setSubtypeGroups([]);
setSelectedGroupId("");
return;
}
setGroupOptionsLoading(true);
try {
const response = await getDocumentSubtypeGroups(
Number(fileType),
loaderData.frontendJWT || undefined,
selectedModuleId,
);
if (cancelled) return;
if ("error" in response || !response.data) {
setSubtypeGroups([]);
setSelectedGroupId("");
return;
}
const groups = response.data;
setSubtypeGroups(groups);
setSelectedGroupId((currentValue) => {
if (groups.some((item) => String(item.id) === currentValue)) {
return currentValue;
}
return groups.length === 1 ? String(groups[0].id) : "";
});
} finally {
if (!cancelled) {
setGroupOptionsLoading(false);
}
}
};
void loadSubtypeGroups();
return () => {
cancelled = true;
};
}, [fileType, loaderData.frontendJWT, selectedModuleId]);
// 处理合同主文件选择
const handleContractMainFilesSelected = (files: FileList) => {
@@ -1260,8 +1325,8 @@ export default function FilesUpload() {
const createdBy = loaderData.userInfo?.user_id as number | undefined;
const uploadResp = await handleFileUpload(
binaryData, mainFile.name, mainFile.type,
fileType as FileType, priority,
region, createdBy, loaderData.frontendJWT || undefined,
fileType as FileType, selectedGroupId ? Number(selectedGroupId) : null, priority,
region, createdBy, attachmentFiles, loaderData.frontendJWT || undefined,
);
if (!uploadResp.success) {
@@ -1350,6 +1415,10 @@ export default function FilesUpload() {
toastService.error('请先选择文件类型');
return;
}
if (subtypeGroups.length > 1 && !selectedGroupId) {
toastService.error('请先选择子类型后再上传');
return;
}
// 检查是否为合同类型
const selectedType = loaderData.documentTypes.find(t => t.id.toString() === fileType);
@@ -1596,8 +1665,8 @@ export default function FilesUpload() {
const createdBy = loaderData.userInfo?.user_id as number | undefined;
const uploadPromise = handleFileUpload(
binaryData, file.name, file.type,
fileType as FileType, priority,
region, createdBy, loaderData.frontendJWT || undefined,
fileType as FileType, selectedGroupId ? Number(selectedGroupId) : null, priority,
region, createdBy, undefined, loaderData.frontendJWT || undefined,
);
const timeoutPromise = new Promise<UploadResult>((_, reject) => {
@@ -2362,27 +2431,66 @@ export default function FilesUpload() {
</select>
<div className="form-tip"></div>
</div>
{/* 子类型(专属类型)- 始终显示 */}
{/* 子类型(二级分组) */}
<div className="form-group">
<label htmlFor="attribute-type-select" className="form-label">
<span className="required">*</span>
</label>
<select
id="attribute-type-select"
name="attributeType"
name="groupId"
className="form-select"
value={attributeType}
onChange={(e) => setAttributeType(e.target.value)}
disabled={uploadStage !== "idle"}
required
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
disabled={uploadStage !== "idle" || !fileType || groupOptionsLoading || subtypeGroups.length <= 1}
required={subtypeGroups.length > 1}
>
{CONTRACT_TYPES.map(type => (
<option key={type.value} value={type.value}>
{type.label}
{!fileType ? (
<option value=""></option>
) : groupOptionsLoading ? (
<option value="">...</option>
) : subtypeGroups.length === 0 ? (
<option value=""></option>
) : subtypeGroups.length === 1 ? (
<option value={String(subtypeGroups[0].id)}>
{getSubtypeDisplayName(subtypeGroups[0])}
</option>
))}
) : (
<>
<option value=""></option>
{subtypeGroups.map((group) => (
<option key={group.id} value={String(group.id)}>
{getSubtypeDisplayName(group)}
</option>
))}
</>
)}
</select>
<div className="form-tip">//</div>
<div className="form-tip">
{!fileType
? "请先选择文件类型,再确定本次上传实际命中的子类型。"
: groupOptionsLoading
? "正在加载当前文档类型下可用的子类型配置。"
: subtypeGroups.length === 0
? "当前文档类型在当前入口下还没有可用子类型,请先到评查点分组管理补齐“一级分组 / 二级分组 / 规则集”绑定。"
: hasMultipleSubtypeGroups
? "同一文档类型在当前入口下已拆分多个子类型,请选择本次上传实际命中的子类型。"
: isSingleDefaultSubtype
? "当前文档类型在当前入口下尚未拆分业务子类型,系统将按默认子类型处理;后续可在评查点分组管理中继续细分。"
: "当前文档类型在当前入口下仅配置了一个子类型,系统会自动带出该子类型。"}
</div>
{selectedRootGroupName ? (
<div className="form-tip">
{selectedRootGroupName}
{selectedEntryModuleName ? ` · 入口模块:${selectedEntryModuleName}` : ""}
</div>
) : null}
{selectedSubtypeGroup ? (
<div className="form-tip">
{getSubtypeDisplayName(selectedSubtypeGroup)}
{selectedSubtypeGroup.displayHint ? ` · ${selectedSubtypeGroup.displayHint}` : ""}
</div>
) : null}
</div>
<div className="form-group">
<label htmlFor="docNumber" className="form-label"></label>
@@ -2783,7 +2891,7 @@ export default function FilesUpload() {
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
<span className="font-medium"></span>
</div>
<p className="text-sm text-green-700"></p>
<p className="text-sm text-green-700"></p>
</div>
{/* <div className="flex justify-end">
@@ -2792,7 +2900,7 @@ export default function FilesUpload() {
icon="ri-file-search-line"
>
查看详情并审核
查看详情
</Button>
</div> */}
</div>