归档拆出来

dev_1.2
tangjie 2026-07-31 14:08:05 +08:00
parent b2465721bb
commit 8c34e650d2
3 changed files with 246 additions and 196 deletions

View File

@ -0,0 +1,222 @@
import React, { useState, useEffect } from "react";
import {
Tag,
Button,
Card,
Table,
Input,
Flex,
Popover,
Image,
Row,
Col,
DatePicker,
Form,
message,
} from "antd";
import dayjs from "dayjs";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib";
const { router } = tools;
const ArchiveContent = ({ readOnly, ...props }) => {
const projectId = router.query?.id;
const { processControlDetail, processControlFiles, evalProjectDetailData } =
props.safetyEvalBusiness || {};
const { processControl } = processControlDetail || {};
const [files, setFiles] = useState([]);
const [archiveForm] = Form.useForm();
const fetchDetail = () => {
props.evalProcessControlDetail({ projectId });
};
const fetchFiles = () => {
props.evalProcessControlFiles({ projectId });
};
useEffect(() => {
if (projectId) {
fetchDetail();
fetchFiles();
}
}, [projectId]);
useEffect(() => {
if (processControlDetail) {
archiveForm.setFieldsValue({
projectNo: evalProjectDetailData.projectNo,
archiveTransferorName: processControlDetail.archiveTransferorName || "",
archiveArchivistName: processControlDetail.archiveArchivistName || "",
archiveDate: processControlDetail.archiveDate
? dayjs(processControlDetail.archiveDate)
: null,
});
}
}, [processControlDetail]);
useEffect(() => {
if (processControlFiles) {
setFiles(processControlFiles);
}
}, [processControlFiles]);
const isSignatureCompleted =
processControl?.reviewerSignatureTaskStatus === "completed";
const canArchive = isSignatureCompleted;
const handleArchive = async () => {
if (!projectId) return;
const values = await archiveForm.validateFields();
const res = await props.evalProcessControlArchive({ projectId, ...values });
if (res?.success !== false) {
message.success("项目已归档入库");
fetchDetail();
props.getDetail?.();
}
};
const isImage = (name) =>
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(name || "");
const fileColumns = [
{ title: "流程名称", dataIndex: "processName", ellipsis: true },
{ title: "条目名称", dataIndex: "itemName", ellipsis: true },
{
title: "文件",
dataIndex: "projectDocUrl",
render: (val) => {
let fileList = [];
if (typeof val === "string") {
try {
fileList = JSON.parse(val);
} catch {
fileList = [];
}
} else if (Array.isArray(val)) {
fileList = val;
}
if (!fileList.length) return "-";
return (
<Flex vertical gap={2}>
<Popover
title="附件列表"
content={
<Flex vertical gap={4}>
{fileList.map((f, i) => {
const url = f.url || f.response?.url;
return isImage(f.name) ? (
<Flex key={f.uid || i} align="center" gap={4}>
{<Image src={url} width={80} />}
</Flex>
) : (
<a
key={f.uid || i}
style={{ cursor: "pointer" }}
onClick={() => window.open(url)}
>
{f.name || "查看文件"}
</a>
);
})}
</Flex>
}
trigger="hover"
placement="topLeft"
>
<a
onClick={(e) => e.preventDefault()}
style={{ cursor: "pointer" }}
>
{fileList.length}个附件
</a>
</Popover>
</Flex>
);
},
},
];
return (
<Card
title="项目归档"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
>
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
项目归档
</Tag>
<span className="pf-desc">
项目过程文件清单确认签字完成后归档入库
</span>
</div>
<Table
dataSource={files}
columns={fileColumns}
rowKey={(record) =>
`${record.processName}-${record.materialName}-${record.itemName}`
}
pagination={false}
size="small"
/>
<Form form={archiveForm} layout="vertical" disabled={readOnly}>
<Row gutter={16} style={{ marginTop: 16, marginBottom: 16 }}>
<Col span={6}>
<Form.Item label="项目编号" name="projectNo">
<Input placeholder="请输入项目编号" disabled />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="移交人" name="archiveTransferorName" rules={[{ required: true, message: "请输入移交人" }]}>
<Input placeholder="请输入移交人" allowClear maxLength={10} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="档案管理员" name="archiveArchivistName" rules={[{ required: true, message: "请输入档案管理员" }]}>
<Input placeholder="请输入档案管理员" allowClear maxLength={10} />
</Form.Item>
</Col>
{processControlDetail?.archiveDate && (
<Col span={6}>
<Form.Item label="入库日期" name="archiveDate">
<DatePicker style={{ width: "100%" }} disabled />
</Form.Item>
</Col>
)}
</Row>
</Form>
<Flex
justify="space-between"
align="center"
style={{ marginTop: 12, paddingBottom: 16 }}
>
{!readOnly && (
<Button
type="primary"
onClick={handleArchive}
loading={props.safetyEvalBusiness?.processControlArchiveLoading}
disabled={!canArchive}
title={
!canArchive
? !isSignatureCompleted
? "需签字完成后才可归档"
: "需生成归档交接单后才可归档"
: ""
}
>
双方签字并确认入库
</Button>
)}
</Flex>
</Card>
);
};
export default Connect([NS_SAFETY_EVAL_BUSINESS], true)(ArchiveContent);

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import {
Tag,
Button,
Card,
Table,
@ -9,15 +8,8 @@ import {
Flex,
Typography,
message,
Popover,
Image,
Collapse,
Row,
Col,
DatePicker,
Form,
} from "antd";
import dayjs from "dayjs";
import { CheckCircleFilled } from "@ant-design/icons";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
@ -28,23 +20,18 @@ const { router } = tools;
const ControlContent = ({ readOnly, ...props }) => {
const projectId = router.query?.id;
const { processControlDetail, processControlFiles, evalProjectDetailData } =
props.safetyEvalBusiness || {};
const { processControlDetail } = props.safetyEvalBusiness || {};
const { processControl, controllers, reviewItems } =
processControlDetail || {};
const [selectPeople, setSelectPeople] = useState(null);
// 当前步骤1-过程控制审核 2-机构负责人签发 3-项目归档
// 当前步骤1-过程控制审核 2-机构负责人签发
const [step, setStep] = useState(1);
// 过程控制审核
const [rows, setRows] = useState([]);
// 项目归档
const [files, setFiles] = useState([]);
const [archiveForm] = Form.useForm();
// 机构负责人签发
const [approvalOpinion, setApprovalOpinion] = useState("");
@ -61,14 +48,9 @@ const ControlContent = ({ readOnly, ...props }) => {
});
};
const fetchFiles = () => {
props.evalProcessControlFiles({ projectId });
};
useEffect(() => {
if (projectId) {
fetchDetail();
fetchFiles();
}
}, [projectId]);
@ -85,25 +67,8 @@ const ControlContent = ({ readOnly, ...props }) => {
if (processControl?.opinionContent) {
setApprovalOpinion(processControl.opinionContent);
}
if (processControl) {
archiveForm.setFieldsValue({
projectNo: evalProjectDetailData.projectNo,
archiveTransferorName: processControlDetail.archiveTransferorName || "",
archiveArchivistName: processControlDetail.archiveArchivistName || "",
archiveDate: processControlDetail.archiveDate
? dayjs(processControlDetail.archiveDate)
: null,
});
}
}, [processControlDetail]);
useEffect(() => {
if (processControlFiles) {
setFiles(processControlFiles);
}
}, [processControlFiles]);
const updateRow = (index, field, value) => {
const next = [...rows];
next[index] = { ...next[index], [field]: value };
@ -112,10 +77,6 @@ const ControlContent = ({ readOnly, ...props }) => {
const isDispatched =
processControl?.reviewerSignatureTaskStatus !== "non_dispatched";
const isSignatureCompleted =
processControl?.reviewerSignatureTaskStatus === "completed";
const canArchive = isSignatureCompleted;
// 过程控制签字提交confirm
const handleConfirm = async (resultCode) => {
@ -165,18 +126,6 @@ const ControlContent = ({ readOnly, ...props }) => {
}
};
// 归档项目并入库
const handleArchive = async () => {
if (!projectId) return;
const values = await archiveForm.validateFields();
const res = await props.evalProcessControlArchive({ projectId, ...values });
if (res?.success !== false) {
message.success("项目已归档入库");
fetchDetail();
props.getDetail?.();
}
};
const reviewColumns = [
{ title: "程序审核项", dataIndex: "reviewDimensionName", width: 120 },
{ title: "核查内容", dataIndex: "reviewFocus", width: 200 },
@ -216,73 +165,9 @@ const ControlContent = ({ readOnly, ...props }) => {
},
];
const isImage = (name) =>
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(name || "");
const fileColumns = [
{ title: "流程名称", dataIndex: "processName", ellipsis: true },
{ title: "条目名称", dataIndex: "itemName", ellipsis: true },
{
title: "文件",
dataIndex: "projectDocUrl",
render: (val) => {
let files = [];
if (typeof val === "string") {
try {
files = JSON.parse(val);
} catch {
files = [];
}
} else if (Array.isArray(val)) {
files = val;
}
if (!files.length) return "-";
return (
<Flex vertical gap={2}>
<Popover
title="附件列表"
content={
<Flex vertical gap={4}>
{files.map((f, i) => {
const url = f.url || f.response?.url;
return isImage(f.name) ? (
<Flex key={f.uid || i} align="center" gap={4}>
{<Image src={url} width={80} />}
</Flex>
) : (
<a
key={f.uid || i}
style={{ cursor: "pointer" }}
onClick={() => window.open(url)}
>
{f.name || "查看文件"}
</a>
);
})}
</Flex>
}
trigger="hover"
placement="topLeft"
>
<a
onClick={(e) => e.preventDefault()}
style={{ cursor: "pointer" }}
>
{files.length}个附件
</a>
</Popover>
</Flex>
);
},
},
];
const STEP_ITEMS = [
{ key: 1, label: "过程控制审核" },
{ key: 2, label: "机构负责人签发" },
{ key: 3, label: "项目归档" },
];
return (
@ -327,7 +212,6 @@ const ControlContent = ({ readOnly, ...props }) => {
size="small"
/>
<Collapse
style={{ marginTop: 12 }}
items={[
{
@ -471,82 +355,6 @@ const ControlContent = ({ readOnly, ...props }) => {
</div>
</Card>
)}
{step === 3 && (
<Card
title="项目归档"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
>
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
项目归档
</Tag>
<span className="pf-desc">
项目过程文件清单确认签字完成后归档入库
</span>
</div>
<Table
dataSource={files}
columns={fileColumns}
rowKey={(record) =>
`${record.processName}-${record.materialName}-${record.itemName}`
}
pagination={false}
size="small"
/>
<Form form={archiveForm} layout="vertical" disabled={readOnly}>
<Row gutter={16} style={{ marginTop: 16, marginBottom: 16 }}>
<Col span={6}>
<Form.Item label="项目编号" name="projectNo">
<Input placeholder="请输入项目编号" disabled />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="移交人" name="archiveTransferorName" rules={[{ required: true, message: "请输入移交人" }]}>
<Input placeholder="请输入移交人" allowClear maxLength={10} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="档案管理员" name="archiveArchivistName" rules={[{ required: true, message: "请输入档案管理员" }]}>
<Input placeholder="请输入档案管理员" allowClear maxLength={10} />
</Form.Item>
</Col>
{processControlDetail?.archiveDate&&<Col span={6}>
<Form.Item label="入库日期" name="archiveDate">
<DatePicker style={{ width: "100%" }} disabled />
</Form.Item>
</Col>}
</Row>
</Form>
<Flex
justify="space-between"
align="center"
style={{ marginTop: 12, paddingBottom: 16 }}
>
{!readOnly && (
<Button
type="primary"
onClick={handleArchive}
loading={props.safetyEvalBusiness?.processControlArchiveLoading}
disabled={!canArchive}
title={
!canArchive
? !isSignatureCompleted
? "需签字完成后才可归档"
: "需生成归档交接单后才可归档"
: ""
}
>
双方签字并确认入库
</Button>
)}
</Flex>
</Card>
)}
</div>
);
};

View File

@ -14,6 +14,7 @@ import SurveyContent from "./SurveyContent";
import InternalReviewContent from "./InternalReviewContent";
import TechnicalReviewContent from "./TechnicalReviewContent";
import ControlContent from "./ControlContent";
import ArchiveContent from "./ArchiveContent";
import "./index.less";
const PROCESS_TABS = [
@ -35,6 +36,7 @@ const PROCESS_TABS = [
{ key: "internal", index: "6", label: "内部审核", role: "内部审核人" },
{ key: "technical", index: "7", label: "技术审核", role: "技术负责人" },
{ key: "control", index: "8", label: "过程控制", role: "过程控制负责人" },
{ key: "archive", index: "9", label: "项目归档", role: "档案管理员" },
];
const { router } = tools;
@ -58,7 +60,7 @@ const parseProjectNode = (data) => {
}
};
const buildProcessTabs = (projectNode) => {
const buildProcessTabs = (projectNode, archiveFlag) => {
const nodes = parseProjectNode(projectNode);
const nodeMap = {};
nodes.forEach((n) => {
@ -95,6 +97,18 @@ const buildProcessTabs = (projectNode) => {
};
}
if (idx === 8) {
// 项目归档前8个全部完成才解锁
const unlocked = allPrevDone(8);
const archived = archiveFlag === 1;
return {
...item,
state: unlocked ? (archived ? "已完成" : "待办理") : "前置未完成",
stateType: unlocked ? (archived ? "success" : "default") : "warning",
locked: !unlocked,
};
}
if (node) {
return {
...item,
@ -114,7 +128,10 @@ const ProjectFlow = (props) => {
const projectId = router.query?.id;
const readOnly = router.query?.readOnly === '1';
const processTabs = buildProcessTabs(evalProjectDetailData?.projectNode);
const processTabs = buildProcessTabs(
evalProjectDetailData?.projectNode,
evalProjectDetailData?.archiveFlag,
);
const handleTabChange = (key) => {
if (key === "report") {
@ -167,6 +184,9 @@ const ProjectFlow = (props) => {
case "control":
children = <ControlContent getDetail={getDetail} readOnly={readOnly} />;
break;
case "archive":
children = <ArchiveContent getDetail={getDetail} readOnly={readOnly} />;
break;
default:
children = null;
}