safety-eval-service-frontend/src/pages/Container/SafetyEvalBusiness/ProjectFlow/ControlContent.js

555 lines
17 KiB
JavaScript
Raw Normal View History

2026-07-24 11:05:09 +08:00
import React, { useState, useEffect } from "react";
2026-07-28 17:27:14 +08:00
import {
Tag,
Button,
Card,
Table,
Select,
Input,
Flex,
Typography,
message,
Popover,
Image,
Collapse,
2026-07-28 18:12:06 +08:00
Row,
Col,
DatePicker,
Form,
2026-07-28 17:27:14 +08:00
} from "antd";
2026-07-28 18:12:06 +08:00
import dayjs from "dayjs";
2026-07-24 15:08:29 +08:00
import { CheckCircleFilled } from "@ant-design/icons";
2026-07-24 11:05:09 +08:00
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib";
const { TextArea } = Input;
const { router } = tools;
2026-07-30 09:59:44 +08:00
const ControlContent = ({ readOnly, ...props }) => {
2026-07-24 11:05:09 +08:00
const projectId = router.query?.id;
2026-07-28 18:12:06 +08:00
const { processControlDetail, processControlFiles, evalProjectDetailData } =
2026-07-24 15:08:29 +08:00
props.safetyEvalBusiness || {};
const { processControl, controllers, reviewItems } =
processControlDetail || {};
2026-07-24 11:05:09 +08:00
2026-07-24 15:08:29 +08:00
const [selectPeople, setSelectPeople] = useState(null);
2026-07-28 17:27:14 +08:00
2026-07-24 11:05:09 +08:00
// 当前步骤1-过程控制审核 2-机构负责人签发 3-项目归档
const [step, setStep] = useState(1);
// 过程控制审核
const [rows, setRows] = useState([]);
// 项目归档
const [files, setFiles] = useState([]);
2026-07-28 18:12:06 +08:00
const [archiveForm] = Form.useForm();
2026-07-24 11:05:09 +08:00
2026-07-24 15:08:29 +08:00
// 机构负责人签发
const [approvalOpinion, setApprovalOpinion] = useState("");
2026-07-24 11:05:09 +08:00
const fetchDetail = () => {
2026-07-24 15:08:29 +08:00
props.evalProcessControlDetail({ projectId }).then((res) => {
if (res?.success) {
if (res?.data?.signTaskPersonCo) {
setSelectPeople({
value: res?.data?.signTaskPersonCo?.signerPersonnelId,
label: res?.data?.signTaskPersonCo?.signerName,
});
}
}
});
2026-07-24 11:05:09 +08:00
};
const fetchFiles = () => {
props.evalProcessControlFiles({ projectId });
};
useEffect(() => {
if (projectId) {
fetchDetail();
fetchFiles();
}
}, [projectId]);
useEffect(() => {
if (reviewItems) {
setRows(
reviewItems.map((item) => ({
...item,
_key: item.id || item.reviewDimensionCode,
})),
);
}
2026-07-28 17:27:14 +08:00
2026-07-24 11:05:09 +08:00
if (processControl?.opinionContent) {
2026-07-28 15:10:56 +08:00
setApprovalOpinion(processControl.opinionContent);
2026-07-24 11:05:09 +08:00
}
2026-07-28 18:12:06 +08:00
if (processControl) {
archiveForm.setFieldsValue({
projectNo: evalProjectDetailData.projectNo,
archiveTransferorName: processControlDetail.archiveTransferorName || "",
archiveArchivistName: processControlDetail.archiveArchivistName || "",
archiveDate: processControlDetail.archiveDate
? dayjs(processControlDetail.archiveDate)
: null,
});
}
2026-07-24 11:05:09 +08:00
}, [processControlDetail]);
useEffect(() => {
if (processControlFiles) {
setFiles(processControlFiles);
}
}, [processControlFiles]);
const updateRow = (index, field, value) => {
const next = [...rows];
next[index] = { ...next[index], [field]: value };
setRows(next);
};
2026-07-24 15:08:29 +08:00
const isDispatched =
processControl?.reviewerSignatureTaskStatus !== "non_dispatched";
const isSignatureCompleted =
processControl?.reviewerSignatureTaskStatus === "completed";
2026-07-28 17:27:14 +08:00
const canArchive = isSignatureCompleted;
2026-07-24 11:05:09 +08:00
// 过程控制签字提交confirm
const handleConfirm = async (resultCode) => {
if (!projectId) return;
2026-07-28 17:27:14 +08:00
if (!rows.every((r) => r.reviewConclusion)) {
2026-07-28 15:10:56 +08:00
message.error("请填写所有核查项的结果");
return;
}
2026-07-24 11:05:09 +08:00
const payload = {
id: processControl?.id,
projectId,
resultCode,
2026-07-28 15:10:56 +08:00
opinionContent: approvalOpinion,
2026-07-24 11:05:09 +08:00
reviewItems: rows.map((r) => ({
bizType: "eval_process_control",
bizId: processControl?.id,
reviewDimensionCode: r.reviewDimensionCode,
reviewDimensionName: r.reviewDimensionName,
reviewFocus: r.reviewFocus,
reviewConclusion: r.reviewConclusion,
reviewOpinion: r.reviewOpinion,
})),
};
const res = await props.evalProcessControlConfirm(payload);
if (res?.success !== false) {
2026-07-24 15:08:29 +08:00
message.success(
resultCode === "passed" ? "已提交通过审核" : "已提交退回修改",
);
2026-07-28 17:27:14 +08:00
fetchDetail();
2026-07-24 11:05:09 +08:00
}
};
// 下发签字任务assign
const handleAssign = async () => {
if (!projectId) return;
2026-07-24 15:08:29 +08:00
const res = await props.evalProcessControlAssign({
projectId,
signTaskPersonCmd: {
signerPersonnelId: selectPeople?.value,
signerName: selectPeople?.label,
},
opinionContent: approvalOpinion,
});
2026-07-24 11:05:09 +08:00
if (res?.success !== false) {
message.success("已下发签字任务");
fetchDetail();
}
};
// 归档项目并入库
const handleArchive = async () => {
if (!projectId) return;
2026-07-28 18:12:06 +08:00
const values = await archiveForm.validateFields();
const res = await props.evalProcessControlArchive({ projectId, ...values });
2026-07-24 11:05:09 +08:00
if (res?.success !== false) {
message.success("项目已归档入库");
fetchDetail();
2026-07-24 17:20:37 +08:00
props.getDetail?.();
2026-07-24 11:05:09 +08:00
}
};
const reviewColumns = [
2026-07-24 15:08:29 +08:00
{ title: "程序审核项", dataIndex: "reviewDimensionName", width: 120 },
{ title: "核查内容", dataIndex: "reviewFocus", width: 200 },
2026-07-24 11:05:09 +08:00
{
2026-07-24 15:08:29 +08:00
title: "结果",
2026-07-24 11:05:09 +08:00
dataIndex: "reviewConclusion",
2026-07-28 15:10:56 +08:00
width: 150,
2026-07-24 11:05:09 +08:00
render: (val, _, idx) => (
<Select
value={val}
size="small"
2026-07-28 15:10:56 +08:00
style={{ width: 130 }}
allowClear
2026-07-30 09:59:44 +08:00
disabled={readOnly}
2026-07-28 15:10:56 +08:00
placeholder="请选择结果"
2026-07-24 11:05:09 +08:00
onChange={(v) => updateRow(idx, "reviewConclusion", v)}
>
2026-07-24 15:08:29 +08:00
<Select.Option value="conform_to">符合</Select.Option>
<Select.Option value="not_conform_to">不符合</Select.Option>
2026-07-24 11:05:09 +08:00
</Select>
),
},
{
2026-07-24 15:08:29 +08:00
title: "意见",
2026-07-24 11:05:09 +08:00
dataIndex: "reviewOpinion",
render: (val, _, idx) => (
<Input
size="small"
value={val}
2026-07-24 15:08:29 +08:00
allowClear
2026-07-30 09:59:44 +08:00
disabled={readOnly}
2026-07-24 15:08:29 +08:00
maxLength={60}
placeholder="请输入意见"
2026-07-24 11:05:09 +08:00
onChange={(e) => updateRow(idx, "reviewOpinion", e.target.value)}
/>
),
},
];
2026-07-24 15:27:56 +08:00
const isImage = (name) =>
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(name || "");
2026-07-24 11:05:09 +08:00
const fileColumns = [
2026-07-24 15:08:29 +08:00
{ title: "流程名称", dataIndex: "processName", ellipsis: true },
{ title: "条目名称", dataIndex: "itemName", ellipsis: true },
2026-07-28 17:27:14 +08:00
2026-07-24 11:05:09 +08:00
{
2026-07-24 15:08:29 +08:00
title: "文件",
2026-07-24 11:05:09 +08:00
dataIndex: "projectDocUrl",
2026-07-28 17:27:14 +08:00
2026-07-24 15:08:29 +08:00
render: (val) => {
let files = [];
if (typeof val === "string") {
2026-07-28 17:27:14 +08:00
try {
files = JSON.parse(val);
} catch {
files = [];
}
2026-07-24 15:08:29 +08:00
} else if (Array.isArray(val)) {
files = val;
}
if (!files.length) return "-";
return (
<Flex vertical gap={2}>
2026-07-28 17:27:14 +08:00
<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)}
>
2026-07-24 15:27:56 +08:00
{f.name || "查看文件"}
2026-07-28 17:27:14 +08:00
</a>
);
})}
</Flex>
}
trigger="hover"
placement="topLeft"
>
<a
onClick={(e) => e.preventDefault()}
style={{ cursor: "pointer" }}
2026-07-24 15:08:29 +08:00
>
2026-07-28 17:27:14 +08:00
{files.length}个附件
</a>
</Popover>
2026-07-24 15:08:29 +08:00
</Flex>
);
},
2026-07-24 11:05:09 +08:00
},
];
const STEP_ITEMS = [
{ key: 1, label: "过程控制审核" },
{ key: 2, label: "机构负责人签发" },
{ key: 3, label: "项目归档" },
];
return (
<div>
2026-07-24 15:08:29 +08:00
<Flex
gap={0}
style={{ marginBottom: 16, borderBottom: "1px solid #e8e8e8" }}
>
2026-07-24 11:05:09 +08:00
{STEP_ITEMS.map((item) => (
<Button
key={item.key}
type="text"
onClick={() => setStep(item.key)}
style={{
minWidth: 128,
2026-07-24 15:08:29 +08:00
borderBottom:
step === item.key
? "3px solid #1677ff"
: "3px solid transparent",
2026-07-24 11:05:09 +08:00
borderRadius: 0,
fontWeight: step === item.key ? 700 : 400,
color: step === item.key ? "#1677ff" : undefined,
background: step === item.key ? "#e6f4ff" : "transparent",
}}
>
{item.label}
</Button>
))}
</Flex>
{step === 1 && (
<Card
title="过程控制审核"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
>
<Table
dataSource={rows}
columns={reviewColumns}
rowKey="_key"
pagination={false}
size="small"
/>
2026-07-28 17:27:14 +08:00
<Collapse
style={{ marginTop: 12 }}
items={[
{
key: "checklist",
label: "过程控制完整核查清单13项",
children: (
<ol
style={{
margin: 0,
paddingLeft: 20,
fontSize: 13,
color: "#475569",
lineHeight: 1.8,
}}
>
<li>合同签订前是否进行项目风险分析</li>
<li>是否签订技术服务合同</li>
<li>是否下达项目任务通知单</li>
<li>是否有项目组成立和项目负责人任命记录</li>
<li>是否编制项目实施计划</li>
<li>现场检查记录是否完整有效</li>
<li>是否有书面整改通知单并由被评价单位盖章或签字确认</li>
<li>
是否进行现场复查复查单是否双方签名或盖章并注明日期
</li>
<li>是否有现场勘查整改复查影像资料</li>
<li>是否完成内审技审并保留审核记录签字和日期</li>
<li>委托方提供资料是否齐全并由被评价单位盖章</li>
<li>
委托方资料评价过程资料和评价报告存档是否符合要求
</li>
<li>存档资料是否完整且具有可追溯性</li>
</ol>
),
},
]}
/>
2026-07-24 15:08:29 +08:00
<Flex justify="flex-end" gap={8} style={{ padding: "12px 0" }}>
2026-07-30 09:59:44 +08:00
{!readOnly && (
<Button
type="primary"
onClick={() => handleConfirm("passed")}
loading={props.safetyEvalBusiness?.processControlConfirmLoading}
>
过程控制签字提交
</Button>
)}
2026-07-24 11:05:09 +08:00
</Flex>
</Card>
)}
{step === 2 && (
<Card
title="机构负责人签发"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
>
2026-07-24 15:08:29 +08:00
<div className="final-approval-card">
<div
style={{
marginBottom: 12,
display: "flex",
alignItems: "center",
gap: 8,
}}
2026-07-24 11:05:09 +08:00
>
2026-07-24 15:08:29 +08:00
<CheckCircleFilled style={{ color: "#52c41a", fontSize: 16 }} />
<span>无可能引发法律纠纷的重大风险</span>
</div>
<div
style={{
marginBottom: 12,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<CheckCircleFilled style={{ color: "#52c41a", fontSize: 16 }} />
<span>评价过程无违规现象</span>
</div>
<div
style={{
marginBottom: 12,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<CheckCircleFilled style={{ color: "#52c41a", fontSize: 16 }} />
<span>全过程记录和签字完整</span>
</div>
<div style={{ marginBottom: 12 }}>
<Typography.Text strong>签发意见</Typography.Text>
<TextArea
rows={3}
value={approvalOpinion}
onChange={(e) => setApprovalOpinion(e.target.value)}
2026-07-30 09:59:44 +08:00
disabled={readOnly}
2026-07-24 15:08:29 +08:00
placeholder="请输入签发意见"
style={{ marginTop: 4 }}
/>
</div>
<Flex
justify="space-between"
align="center"
style={{ paddingBottom: 16 }}
>
<Flex gap={8} align="center">
<Typography.Text type="secondary">
评价机构负责人
</Typography.Text>
<Select
placeholder="请选择评价机构负责人"
size="small"
allowClear
style={{ width: 180 }}
2026-07-30 09:59:44 +08:00
disabled={isDispatched || readOnly}
2026-07-24 15:08:29 +08:00
value={selectPeople?.value}
onChange={(value, option) => {
setSelectPeople(option);
}}
options={(controllers || []).map((r) => ({
value: r.personnelId,
label: r.personnelName,
}))}
/>
<Button
size="small"
onClick={handleAssign}
loading={
props.safetyEvalBusiness?.processControlAssignLoading
}
2026-07-30 09:59:44 +08:00
disabled={isDispatched || !selectPeople?.value || readOnly}
2026-07-24 15:08:29 +08:00
>
{isDispatched ? "已下发APP签字" : "下发APP签字"}
</Button>
</Flex>
</Flex>
</div>
2026-07-24 11:05:09 +08:00
</Card>
)}
{step === 3 && (
<Card
title="项目归档"
style={{ marginBottom: 16 }}
styles={{ body: { paddingBottom: 0 } }}
>
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
项目归档
</Tag>
2026-07-24 15:08:29 +08:00
<span className="pf-desc">
项目过程文件清单确认签字完成后归档入库
</span>
2026-07-24 11:05:09 +08:00
</div>
<Table
dataSource={files}
columns={fileColumns}
2026-07-24 15:08:29 +08:00
rowKey={(record) =>
`${record.processName}-${record.materialName}-${record.itemName}`
}
2026-07-24 11:05:09 +08:00
pagination={false}
size="small"
/>
2026-07-30 09:59:44 +08:00
<Form form={archiveForm} layout="vertical" disabled={readOnly}>
2026-07-28 18:12:06 +08:00
<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>
<Col span={6}>
<Form.Item label="入库日期" name="archiveDate">
<DatePicker style={{ width: "100%" }} disabled />
</Form.Item>
</Col>
</Row>
</Form>
2026-07-24 15:08:29 +08:00
<Flex
justify="space-between"
align="center"
style={{ marginTop: 12, paddingBottom: 16 }}
>
2026-07-30 09:59:44 +08:00
{!readOnly && (
<Button
type="primary"
onClick={handleArchive}
loading={props.safetyEvalBusiness?.processControlArchiveLoading}
disabled={!canArchive}
title={
!canArchive
? !isSignatureCompleted
? "需签字完成后才可归档"
: "需生成归档交接单后才可归档"
: ""
}
>
双方签字并确认入库
</Button>
)}
2026-07-24 11:05:09 +08:00
</Flex>
</Card>
)}
</div>
);
};
2026-07-24 15:08:29 +08:00
export default Connect([NS_SAFETY_EVAL_BUSINESS], true)(ControlContent);