2026-07-23 17:41:28 +08:00
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message } from "antd";
|
|
|
|
|
|
import { CheckCircleFilled, DownloadOutlined } 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";
|
2026-07-24 18:21:06 +08:00
|
|
|
|
import {ROLE_DEFINITIONS_MAP} from '~/enumerate/constant'
|
2026-07-23 17:41:28 +08:00
|
|
|
|
import "./index.less";
|
2026-07-24 18:21:06 +08:00
|
|
|
|
import dayjs from "dayjs";
|
2026-07-23 17:41:28 +08:00
|
|
|
|
|
|
|
|
|
|
const { router } = tools;
|
2026-07-20 15:40:12 +08:00
|
|
|
|
|
|
|
|
|
|
const STAGES = [
|
2026-07-23 17:41:28 +08:00
|
|
|
|
{ key: "notice", index: "01", label: "从业告知" },
|
|
|
|
|
|
{ key: "inspection", index: "02", label: "现场勘查" },
|
|
|
|
|
|
{ key: "rectification", index: "03", label: "整改通知" },
|
|
|
|
|
|
{ key: "enterprise", index: "04", label: "企业整改" },
|
|
|
|
|
|
{ key: "review", index: "05", label: "整改复查" },
|
2026-07-20 15:40:12 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const ATTACH_TYPE = {
|
|
|
|
|
|
notice: "PRACTICE_NOTIFICATION",
|
|
|
|
|
|
inspection: "SITE_INSPECTION_FORM",
|
|
|
|
|
|
rectification: "RECTIFICATION_NOTICE",
|
|
|
|
|
|
enterprise: "ENTERPRISE_RECTIFICATION",
|
|
|
|
|
|
review: "RECTIFICATION_REVIEW",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const downloadTemplate = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch("https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a608cb30dc53ed98d0b13ae.pdf");
|
|
|
|
|
|
const blob = await res.blob();
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
a.download = "从业告知书模板.pdf";
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("下载失败,请重试");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-20 15:40:12 +08:00
|
|
|
|
const NoticePanel = () => (
|
|
|
|
|
|
<div>
|
2026-07-23 17:41:28 +08:00
|
|
|
|
<div className="pf-section-header">
|
2026-07-20 15:40:12 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>01</span>
|
|
|
|
|
|
<Typography.Text strong style={{ fontSize: 15 }}>安全评价检测检验机构从业告知书</Typography.Text>
|
|
|
|
|
|
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>支持在线填写后打印;阶段试行期间,以机构盖章版上传归档作为完成依据。</Typography.Paragraph>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ background: "#f0f5ff", padding: "8px 12px", borderRadius: 6, marginBottom: 16, display: "flex", gap: 16, fontSize: 13 }}>
|
|
|
|
|
|
<strong>办理方式</strong>
|
|
|
|
|
|
<span>在线填写用于生成、打印标准告知书</span>
|
|
|
|
|
|
<span>打印盖章后上传扫描件或清晰照片</span>
|
|
|
|
|
|
<Tag color="blue">完成节点必须留存盖章件</Tag>
|
|
|
|
|
|
</div>
|
2026-07-23 17:41:28 +08:00
|
|
|
|
<div className="pf-template-card">
|
|
|
|
|
|
<div className="pf-template-left">
|
|
|
|
|
|
<span className="pf-template-icon">📄</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="pf-template-name">从业告知书模板</div>
|
|
|
|
|
|
<div className="pf-template-desc">PDF 格式,可下载后在线填写并打印盖章</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button type="primary" ghost size="small" icon={<DownloadOutlined />} onClick={downloadTemplate}>
|
|
|
|
|
|
下载模板
|
|
|
|
|
|
</Button>
|
2026-07-20 15:40:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-24 18:21:06 +08:00
|
|
|
|
const InspectionPanel = ({ dataSource = [], evalProjectDetailData = {} }) => {
|
|
|
|
|
|
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 (
|
2026-07-20 15:40:12 +08:00
|
|
|
|
<div>
|
2026-07-23 17:41:28 +08:00
|
|
|
|
<div className="pf-section-header">
|
2026-07-20 15:40:12 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>02</span>
|
|
|
|
|
|
<Typography.Text strong style={{ fontSize: 15 }}>现场人员打卡与检查表</Typography.Text>
|
|
|
|
|
|
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>主要核验项目组人员是否真实到场,留存APP人脸、GPS、签到签退、现场照片及本人签字记录。</Typography.Paragraph>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Tag color="success">打卡与检查表完整</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Card type="inner" size="small" title="人员现场打卡情况" style={{ marginBottom: 12 }}>
|
|
|
|
|
|
<Table
|
2026-07-24 18:21:06 +08:00
|
|
|
|
dataSource={dataSource}
|
2026-07-20 15:40:12 +08:00
|
|
|
|
columns={[
|
2026-07-24 18:21:06 +08:00
|
|
|
|
{ title: "人员", dataIndex: "personnelName" },
|
|
|
|
|
|
{ title: "项目角色", dataIndex: "role", render: (v) => Array.isArray(v) ? v.map(item => ROLE_DEFINITIONS_MAP[item]).join('、') : v,ellipsis: true },
|
|
|
|
|
|
{ title: "人脸识别", dataIndex: "faceState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "通过" : "未通过"}</Tag> },
|
|
|
|
|
|
{ title: "GPS定位", dataIndex: "gpsState", render: (v) => <Tag color={v === 1 ? "success" : "error"}>{v === 1 ? "正常" : "异常"}</Tag> },
|
|
|
|
|
|
{ title: "签到时间", dataIndex: "singInTime" },
|
|
|
|
|
|
{ title: "签退时间", dataIndex: "signOutTime" },
|
|
|
|
|
|
{ title: "在场时长", render: (_, r) => calcDuration(r.singInTime, r.signOutTime) },
|
|
|
|
|
|
{ title: "现场照片", dataIndex: "sceneUrl", render: (v) => {
|
|
|
|
|
|
const count = v ? v.split(',').filter(Boolean).length : 0;
|
|
|
|
|
|
return <Button type="link" size="small">{count}张</Button>;
|
|
|
|
|
|
}},
|
|
|
|
|
|
{ title: "本人签字", dataIndex: "signatureUrl", render: (v) => <Tag color={v ? "success" : "default"}>{v ? "已签字" : "未签字"}</Tag> },
|
2026-07-20 15:40:12 +08:00
|
|
|
|
]}
|
2026-07-24 18:21:06 +08:00
|
|
|
|
rowKey="id"
|
2026-07-20 15:40:12 +08:00
|
|
|
|
pagination={false}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div style={{ display: "flex", gap: 24, marginTop: 8, fontSize: 12 }}>
|
2026-07-24 18:21:06 +08:00
|
|
|
|
<span>打卡企业地址:{evalProjectDetailData?.customerAddress || '-'}</span>
|
|
|
|
|
|
<span>允许打卡范围:企业地址周边{evalProjectDetailData?.checkinRadiusMeters || '-'}米</span>
|
|
|
|
|
|
<span>签到日期:{dataSource?.[0]?.singInTime ? dayjs(dataSource?.[0]?.singInTime).format('YYYY-MM-DD') : '-'}</span>
|
2026-07-20 15:40:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-07-24 18:21:06 +08:00
|
|
|
|
};
|
2026-07-20 15:40:12 +08:00
|
|
|
|
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const SimpleUploadPanel = ({ index, title, desc }) => (
|
2026-07-20 15:40:12 +08:00
|
|
|
|
<div>
|
2026-07-23 17:41:28 +08:00
|
|
|
|
<div className="pf-section-header">
|
2026-07-20 15:40:12 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>{index}</span>
|
|
|
|
|
|
<Typography.Text strong style={{ fontSize: 15 }}>{title}</Typography.Text>
|
|
|
|
|
|
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>{desc}</Typography.Paragraph>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-20 15:03:26 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const SurveyContent = (props) => {
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-07-20 15:40:12 +08:00
|
|
|
|
const [activeStage, setActiveStage] = useState("notice");
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const [attachMap, setAttachMap] = useState({});
|
2026-07-24 18:21:06 +08:00
|
|
|
|
const [surveyPersonnel, setSurveyPersonnel] = useState([]);
|
|
|
|
|
|
const { evalSurveyListLoading, evalSurveySaveAttachLoading, evalProjectDetailData } = props.safetyEvalBusiness;
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const projectId = router.query?.id;
|
|
|
|
|
|
const currentIdx = STAGES.findIndex((s) => s.key === activeStage);
|
|
|
|
|
|
const isFirst = currentIdx === 0;
|
|
|
|
|
|
const isLast = currentIdx === STAGES.length - 1;
|
|
|
|
|
|
|
|
|
|
|
|
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 = {};
|
|
|
|
|
|
Object.entries(ATTACH_TYPE).forEach(([, typeCode]) => {
|
|
|
|
|
|
values[typeCode] = map[typeCode] || [];
|
|
|
|
|
|
});
|
|
|
|
|
|
form.setFieldsValue(values);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadAttachments();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-07-24 18:21:06 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (projectId) {
|
|
|
|
|
|
props.evalSurveyPage({ projectId, size: 100 }).then((res) => {
|
|
|
|
|
|
if (res?.success) {
|
|
|
|
|
|
setSurveyPersonnel(res.data || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [projectId]);
|
|
|
|
|
|
|
2026-07-23 17:41:28 +08:00
|
|
|
|
const goPrev = () => {
|
|
|
|
|
|
if (currentIdx > 0) setActiveStage(STAGES[currentIdx - 1].key);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const goNext = () => {
|
|
|
|
|
|
if (currentIdx < STAGES.length - 1) setActiveStage(STAGES[currentIdx + 1].key);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
|
const values = form.getFieldsValue();
|
|
|
|
|
|
const payload = [];
|
|
|
|
|
|
for (const [key, typeCode] of Object.entries(ATTACH_TYPE)) {
|
|
|
|
|
|
const fileList = values[typeCode] || [];
|
|
|
|
|
|
const validFiles = fileList.filter((f) => f.status === "done" && f.url);
|
|
|
|
|
|
if (validFiles.length === 0) {
|
|
|
|
|
|
setActiveStage(key);
|
|
|
|
|
|
message.warning(`请先在"${STAGES.find((s) => s.key === key).label}"步骤上传附件`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
payload.push({
|
|
|
|
|
|
projectId,
|
|
|
|
|
|
attachTypeCode: typeCode,
|
|
|
|
|
|
fileUrl: JSON.stringify(fileList),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await props.evalSurveySaveAttach(payload);
|
|
|
|
|
|
if (res?.success !== false) {
|
|
|
|
|
|
message.success("保存成功");
|
|
|
|
|
|
props.getDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-20 15:40:12 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
2026-07-23 17:41:28 +08:00
|
|
|
|
<Steps
|
|
|
|
|
|
current={currentIdx}
|
|
|
|
|
|
onChange={(idx) => setActiveStage(STAGES[idx].key)}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
items={STAGES.map((s) => ({ title: s.label }))}
|
|
|
|
|
|
/>
|
2026-07-20 15:40:12 +08:00
|
|
|
|
|
2026-07-24 18:21:06 +08:00
|
|
|
|
{activeStage === "notice" && <NoticePanel />}
|
|
|
|
|
|
{activeStage === "inspection" && <InspectionPanel dataSource={surveyPersonnel} evalProjectDetailData={evalProjectDetailData} />}
|
|
|
|
|
|
{activeStage === "rectification" && (
|
|
|
|
|
|
<SimpleUploadPanel
|
|
|
|
|
|
index="03" title="整改通知单上传"
|
|
|
|
|
|
desc="本节点不在线编制整改通知,只归档机构已出具的《整改通知单》文件;上传成功即完成,后续可替换修改。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{activeStage === "enterprise" && (
|
|
|
|
|
|
<SimpleUploadPanel
|
|
|
|
|
|
index="04" title="企业整改报告上传"
|
|
|
|
|
|
desc="企业线下提交《整改报告》后,由机构人员上传归档;本系统不提供企业在线提交和逐项整改流程。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{activeStage === "review" && (
|
|
|
|
|
|
<SimpleUploadPanel
|
|
|
|
|
|
index="05" title="整改复查单上传"
|
|
|
|
|
|
desc="机构人员现场复查后,将经双方签字确认的《整改复查单》上传归档;上传后本节点自动标记完成。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-07-23 17:41:28 +08:00
|
|
|
|
|
|
|
|
|
|
<Form form={form} layout="vertical">
|
|
|
|
|
|
{Object.entries(ATTACH_TYPE).map(([key, typeCode]) => (
|
|
|
|
|
|
<div key={key} style={{ display: key === activeStage ? "block" : "none" }}>
|
|
|
|
|
|
<AttachmentUpload
|
|
|
|
|
|
name={typeCode}
|
|
|
|
|
|
label="上传附件"
|
|
|
|
|
|
maxCount={10}
|
|
|
|
|
|
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xls,.xlsx"
|
|
|
|
|
|
extra="支持 PDF、Word、JPG、PNG、Excel 格式,最多上传 10 个文件"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
|
|
<Flex justify="flex-end" gap={8} style={{ marginTop: 16 }}>
|
|
|
|
|
|
{!isFirst && <Button onClick={goPrev} disabled={evalSurveySaveAttachLoading}>上一步</Button>}
|
|
|
|
|
|
{isLast ? (
|
|
|
|
|
|
<Button type="primary" onClick={handleSave} loading={evalSurveySaveAttachLoading}>保存</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button type="primary" onClick={goNext} disabled={evalSurveySaveAttachLoading}>下一步</Button>
|
2026-07-20 15:40:12 +08:00
|
|
|
|
)}
|
2026-07-23 17:41:28 +08:00
|
|
|
|
</Flex>
|
2026-07-20 15:40:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-23 17:41:28 +08:00
|
|
|
|
export default Connect(
|
|
|
|
|
|
[NS_SAFETY_EVAL_BUSINESS],
|
|
|
|
|
|
true,
|
|
|
|
|
|
)(SurveyContent);
|