import React, { useState, useEffect } from "react";
import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message, Popover, Image, Checkbox } from "antd";
import { DownloadOutlined, QuestionOutlined, PictureOutlined } from "@ant-design/icons";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib";
import AttachmentUpload from "~/components/AttachmentUpload";
import { ROLE_DEFINITIONS_MAP, ROLE_DEFINITIONS } from '~/enumerate/constant'
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP } from "~/enumerate/enterpriseOptions";
import LayoutFooterPortal from "~/components/LayoutFooterPortal";
import { fillDocxTemplate } from "~/utils/fillDocxTemplate";
import "./index.less";
import dayjs from "dayjs";
const { router } = tools;
const STAGES = [
{ key: "PRACTICE_NOTIFICATION", index: "01", label: "从业告知" },
{ key: "SITE_INSPECTION_FORM", index: "02", label: "现场勘查" },
{ key: "RECTIFICATION_NOTICE", index: "03", label: "整改通知" },
{ key: "ENTERPRISE_RECTIFICATION", index: "04", label: "企业整改" },
{ key: "RECTIFICATION_REVIEW", index: "05", label: "整改复查" },
];
/** 各步骤附件必传校验:至少一个文件上传完成;有文件上传中时提示稍后 */
const attachRequiredRules = (label) => [
{
required: true,
validator: (_, value) => {
const fileList = value || [];
if (fileList.some((f) => f.status === "uploading")) {
return Promise.reject(new Error("文件上传中,请稍后再操作"));
}
return fileList.some((f) => f.status === "done" && f.url)
? Promise.resolve()
: Promise.reject(new Error(`请上传${label}`));
},
},
];
const NoticePanel = ({ readOnly, downloadTemplate }) => (
01
安全评价检测检验机构从业告知书
支持在线填写后打印;阶段试行期间,以机构盖章版上传归档作为完成依据。
办理方式
在线填写用于生成、打印标准告知书
打印盖章后上传扫描件或清晰照片
完成节点必须留存盖章件
📄
从业告知书模板
{/*
PDF 格式,可下载后在线填写并打印盖章
*/}
} onClick={downloadTemplate}>
下载模板
);
/** 打卡时间后追加人脸照片图标:faceUrl 逗号分隔,第1张为签到、第2张为签退 */
const renderTimeWithFace = (time, record, idx) => {
const url = record?.faceUrl?.split(",")[idx];
if (!url) return time || "-";
return (
{time}
}
title="人脸照片"
trigger="hover"
>
);
};
const InspectionPanel = ({ dataSource = [], evalProjectDetailData = {}, readOnly }) => {
const calcDuration = (signIn, signOut) => {
if (!signIn || !signOut) return '-';
const start = new Date(signIn);
const end = new Date(signOut);
const diff = (end - start) / 60000;
if (diff <= 0) return '-';
return `${Math.floor(diff / 60)}h${Math.round(diff % 60)}m`;
};
return (
02
现场人员打卡与检查表
主要核验项目组人员是否真实到场,留存APP人脸、GPS、签到签退、现场照片及本人签字记录。
打卡与检查表完整
}>
Array.isArray(v) ? v.map(item => ROLE_DEFINITIONS_MAP[item]).join('、') : v, ellipsis: true },
{ title: "人脸识别", dataIndex: "faceState", render: (v) => {v === 1 ? "通过" : "未通过"} },
{ title: "GPS定位", dataIndex: "gpsState", render: (v) => {v === 1 ? "正常" : "异常"} },
{ title: "签到时间", dataIndex: "singInTime", render: (time, record) => { return renderTimeWithFace(time, record, 0); } },
{ title: "签退时间", dataIndex: "signOutTime", render: (time, record) => { return renderTimeWithFace(time, record, 1); } },
{ title: "在场时长", render: (_, r) => calcDuration(r.singInTime, r.signOutTime) },
{
title: "现场照片", dataIndex: "sceneUrl", render: (v) => {
const urls = v ? v.split(',').filter(Boolean) : [];
if (urls.length === 0) return ;
const content = (
{urls.map((url, i) => (
))}
);
return (
);
}
},
{ title: "本人签字", dataIndex: "signatureUrl", render: (v) => {v ? "已签字" : "未签字"} },
]}
rowKey="id"
pagination={false}
size="small"
/>
打卡企业地址:{evalProjectDetailData?.customerAddress || '-'}
允许打卡范围:企业地址周边{evalProjectDetailData?.checkinRadiusMeters || '-'}米
签到日期:{dataSource?.[0]?.singInTime ? dayjs(dataSource?.[0]?.singInTime).format('YYYY-MM-DD') : '-'}
);
};
const RectificationNoticePanel = ({ readOnly }) => (
03
整改通知单上传
本节点不在线编制整改通知,只归档机构已出具的《整改通知单》文件;上传成功即完成,后续可替换修改。
是否存在重大隐患
);
const EnterpriseRectificationPanel = ({ readOnly }) => (
04
企业整改报告上传
企业线下提交《整改报告》后,由机构人员上传归档;本系统不提供企业在线提交和逐项整改流程。
);
const RectificationReviewPanel = ({ readOnly }) => (
05
整改复查单上传
机构人员现场复查后,将经双方签字确认的《整改复查单》上传归档;上传后本节点自动标记完成。
);
const SurveyContent = ({ readOnly, ...props }) => {
const [form] = Form.useForm();
const [activeStage, setActiveStage] = useState("PRACTICE_NOTIFICATION");
const [attachMap, setAttachMap] = useState({});
const [surveyPersonnel, setSurveyPersonnel] = useState([]);
const { evalSurveySaveAttachLoading, evalProjectDetailData } = props.safetyEvalBusiness;
const projectId = router.query?.id;
const currentIdx = STAGES.findIndex((s) => s.key === activeStage);
const isFirst = currentIdx === 0;
const isLast = currentIdx === STAGES.length - 1;
/** 各步骤附件表单值(按 STAGES 顺序),用于 Steps 完成态判断;
* 注意:useWatch 传数组会被解析为嵌套字段路径,多字段需用函数选择器(入参为表单 values) */
const stepFiles = Form.useWatch(
(values) => STAGES.map((s) => values?.[s.key]),
form,
);
const loadAttachments = async () => {
if (!projectId) return;
const res = await props.evalSurveyList({ projectId });
if (res?.success !== false) {
const map = {};
(res?.data || []).forEach((item) => {
const key = item.attachTypeCode;
try {
const parsed = JSON.parse(item.fileUrl);
map[key] = Array.isArray(parsed) ? parsed : [];
} catch {
map[key] = [];
}
});
setAttachMap(map);
// 回填所有步骤的附件到表单
const values = {};
STAGES.forEach((s) => {
values[s.key] = map[s.key] || [];
});
// 回填整改通知单的 checkbox 字段
const rectItem = (res?.data || []).find((item) => item.attachTypeCode === "RECTIFICATION_NOTICE");
values.majorHazardCheckbox = rectItem?.checkbox === 1;
form.setFieldsValue(values);
}
};
const downloadTemplate = async () => {
const orgInfo = JSON.parse(sessionStorage.getItem('orgInfo'));
const industryText = String(evalProjectDetailData.industryCode || "")
.split(",")
.map((code) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[code])
.join("、");
// 项目组成员(同项目组 tab 页数据):姓名/专业/工作任务
const memberRes = await props.evalProjectMemberPage({ projectId });
const members = (memberRes?.data || []).map((item) => ({
name: item.personnelName,
major: item.capacity || "",
task:
item.responsibility ||
(item.role || [])
.map((r) => ROLE_DEFINITIONS.find((d) => d.value === r)?.summary)
.filter(Boolean)
.join("、"),
isLeader: (item.role || []).includes("PROJECT_LEAD"),
}));
// 项目组组长取自项目组成员中的 PROJECT_LEAD,展示同 namesByRole:如"张三(化工)"
const projectLeader = members
.filter((m) => m.isLeader)
.map((m) => `${m.name}${m.major ? `(${m.major})` : ""}`)
.join("、");
const data = {
account: orgInfo.account,
qualificationCertNo: orgInfo.qualificationCertNo,
infoDisclosureUrl: orgInfo.infoDisclosureUrl,
businessAddress: orgInfo.businessAddress,
legalRepresentative: orgInfo.legalRepresentative,
contactName: orgInfo.contactName,
contactPhone: orgInfo.contactPhone,
projectName: evalProjectDetailData.projectName,
projectAddress: evalProjectDetailData.customerAddress || "",
industryText,
projectLeader,
members,
};
try {
await fillDocxTemplate(
'https://gbs-cqaqpj.oss-cn-beijing.aliyuncs.com/jjb/6a913235e4b0b3767a78622d.docx',
data,
`安全评价检测检验机构从业告知书.docx`,
{ download: true },
);
message.success("导出成功");
} catch {
message.error("导出失败,请重试");
}
};
useEffect(() => {
loadAttachments();
}, []);
useEffect(() => {
if (projectId) {
props.evalSurveyPage({ projectId, size: 100 }).then((res) => {
if (res?.success) {
setSurveyPersonnel(res.data || []);
}
});
}
}, [projectId]);
const validateSurveyStep = () => {
if (surveyPersonnel.length < 2) {
message.warning("您的打卡信息未完善,请前往现场在APP中进行定位打卡");
return false;
}
const hasProjectLead = surveyPersonnel.some((p) =>
Array.isArray(p.role) ? p.role.includes("PROJECT_LEAD") : p.role === "PROJECT_LEAD"
);
if (!hasProjectLead) {
message.warning("您的打卡信息未完善,请前往现场在APP中进行定位打卡");
return false;
}
return true;
};
const goPrev = () => {
if (currentIdx > 0) setActiveStage(STAGES[currentIdx - 1].key);
};
const goNext = () => {
if (currentIdx === STAGES.length - 1) return;
if (activeStage === "SITE_INSPECTION_FORM" && !validateSurveyStep()) return;
setActiveStage(STAGES[currentIdx + 1].key);
};
const handleSave = async () => {
const values = form.getFieldsValue();
// 检查是否有文件正在上传中
const hasUploading = STAGES.some((s) => {
const fileList = values[s.key] || [];
return fileList.some((f) => f.status === "uploading");
});
if (hasUploading) {
message.warning("文件上传中,请稍后再保存");
return;
}
if (!validateSurveyStep()) return;
const payload = [];
for (const s of STAGES) {
const fileList = values[s.key] || [];
const validFiles = fileList.filter((f) => f.status === "done" && f.url);
if (validFiles.length === 0) {
setActiveStage(s.key);
message.warning(`请先在"${s.label}"步骤上传附件`);
return;
}
}
for (const s of STAGES) {
const fileList = values[s.key] || [];
const validFiles = fileList.filter((f) => f.status === "done" && f.url);
if (validFiles.length === 0) continue;
payload.push({
projectId,
attachTypeCode: s.key,
fileUrl: JSON.stringify(fileList),
...(s.key === "RECTIFICATION_NOTICE"
? { checkbox: values.majorHazardCheckbox ? 1 : 0 }
: {}),
});
}
const res = await props.evalSurveySaveAttach(payload);
if (res?.success !== false) {
message.success("保存成功");
props.getDetail();
}
};
return (
setActiveStage(STAGES[idx].key)}
style={{ marginBottom: 16 }}
items={STAGES.map((s, idx) => {
const uploaded = (stepFiles?.[idx] || []).some(
(f) => f.status === "done" && f.url,
);
return {
title: s.label,
...(uploaded ? { status: "finish" } : { status: "wait" }),
};
})}
/>
{!isFirst && }
{isLast ? (
!readOnly &&
) : (
)}
);
};
export default Connect(
[NS_SAFETY_EVAL_BUSINESS],
true,
)(SurveyContent);