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 格式,可下载后在线填写并打印盖章
*/}
); /** 打卡时间后追加人脸照片图标: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);