import React, { useEffect, useMemo, useState } from "react"; import { Form, Table, Button, Modal, Input, Select, InputNumber, Row, Col, message, DatePicker, Space, ConfigProvider, } from "antd"; import { DownloadOutlined, ExportOutlined } from "@ant-design/icons"; import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import SearchForm from "~/components/SearchForm"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; import { NS_REGULATOR_EVAL_REPORT, NS_ORG_INFO } from "~/enumerate/namespace"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { tools } from "@cqsjjb/jjb-common-lib"; import { renderDocxBlob, fillDocxTemplate } from "~/utils/fillDocxTemplate"; import SpotCheckDetailModal from "./components/SpotCheckDetailModal"; import PreviewUrlButton from "~/components/PreviewUrlButton"; import dayjs from "dayjs"; import "./index.less"; const { router } = tools; const { TextArea } = Input; const { RangePicker } = DatePicker; const API_HOST = window.process?.env?.app?.API_HOST || ""; /** 监督检查单 docx 模板(线上) */ const CHECKLIST_TEMPLATE_URL = "https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a7c36c2e4b0435c62b76a34.docx"; /** 原型 CSS:--radius 8 / --radius-lg 12 */ const PROTO_THEME = { token: { borderRadius: 8, borderRadiusLG: 12, borderRadiusSM: 6 }, }; const EVAL_TYPE_OPTIONS = [ { label: "安全预评价", value: "PRE" }, { label: "安全验收评价", value: "ACCEPT" }, { label: "安全现状评价", value: "STATUS" }, ]; /** 监管端原型行业筛选项(编码对齐 OpenAPI 示例 HAZCHEM) */ const INDUSTRY_OPTIONS = [ { label: "化工", value: "HAZCHEM" }, { label: "矿山", value: "MINE" }, { label: "工贸", value: "INDUSTRY_TRADE" }, { label: "建筑施工", value: "CONSTRUCTION" }, ]; /** 抽检-检查类型(对齐原型) */ const CHECK_TYPE_OPTIONS = [ { label: "安全评价报告质量抽查", value: "QUALITY" }, { label: "专项监督检查", value: "SPECIAL" }, { label: "年度随机抽查", value: "ANNUAL" }, ]; const getYearOptions = () => { const year = dayjs().year(); return [0, 1, 2].map((offset) => { const y = year - offset; return { label: String(y), value: y }; }); }; const getOptionLabel = (options, value) => options.find((item) => item.value === value)?.label || value || "-"; /** 文件流下载(批量 ZIP / 导出 CSV) */ const downloadBinaryPost = async (path, body, fallbackName) => { const res = await fetch(`${API_HOST}${path}`, { method: "POST", headers: { "Content-Type": "application/json", token: sessionStorage.getItem("token") || "", }, body: JSON.stringify(body), }); const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { const json = await res.json(); throw new Error(json.errMessage || json.message || "操作失败"); } if (!res.ok) { throw new Error(`请求失败(${res.status})`); } const blob = await res.blob(); let fileName = fallbackName; const disposition = res.headers.get("content-disposition") || ""; const matched = disposition.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i); if (matched?.[1]) { try { fileName = decodeURIComponent(matched[1].replace(/"/g, "")); } catch { fileName = matched[1].replace(/"/g, ""); } } const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = fileName; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); }; /** 单文件本地下载(不在线打开) */ const downloadFileByUrl = async (fileUrl, fileName) => { const response = await fetch(fileUrl, { headers: { token: sessionStorage.getItem("token") || "" }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = fileName || "报告文件"; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); }; /** OpenAPI:AI 分析为前端静态;文案对齐原型 reportAiAnalysis */ const buildStaticAiAnalysis = (record) => ({ overview: "已完成报告全文结构化识别,共识别186页、32项法规标准、18类危险有害因素和46条对策措施。", metrics: [ { label: "综合关注度", value: "中" }, { label: "质量参考分", value: "86" }, { label: "重点提示", value: "2项" }, { label: "一般建议", value: "4项" }, ], rows: [ { dim: "法规标准", result: "识别32项,2项版本建议核验", level: "一般", advice: "核对GB 50016引用版本及地方标准有效性", }, { dim: "危险辨识", result: "主要危险因素覆盖,提示液氨装卸泄漏场景描述不足", level: "重点", advice: "复核危险因素章节与现场储存、装卸设施是否一致", }, { dim: "评价方法", result: "方法与评价单元基本匹配", level: "低", advice: "核对定量评价参数来源和计算附件", }, { dim: "对策措施", result: "4条措施表述较通用", level: "一般", advice: "补充责任主体、实施条件和可验证指标", }, { dim: "数据一致性", result: "发现3处表格数据与正文表述差异", level: "重点", advice: "核查安全距离、设备数量和人员统计数据", }, { dim: "评价结论", result: "结论与章节分析基本一致", level: "低", advice: "结合重点提示完成最终人工判断", }, ], conclusion: "报告整体结构完整,建议重点复核法规版本、液氨装卸场景及3处数据一致性问题。本结果仅供辅助监管使用。", reportLabel: record?.reportNo || record?.reportName || "", }); /** 原型 .reg-modal-section */ const ModalSection = ({ title, desc, children }) => (

{title}

{desc ?

{desc}

: null} {children}
); /** 原型 tableHtml:字段表 / 数据表;kv=字段-内容两列表 */ const ProtoTable = ({ headers, rows, kv = false }) => (
{headers.map((h) => ( ))} {rows.map((row, idx) => ( {row.map((cell, cIdx) => ( ))} ))}
{h}
{cell}
); const getFileExtLabel = (name = "") => { const ext = String(name).split(".").pop(); return ext && ext !== name ? ext.toUpperCase() : "PDF"; }; /** 解析存储的 JSON 文件列表字符串,返回数组 */ const parseFileList = (jsonStr) => { if (!jsonStr) return []; try { const arr = JSON.parse(jsonStr); return Array.isArray(arr) ? arr : []; } catch { return []; } }; const EvalReportDatabase = (props) => { const [searchForm] = Form.useForm(); const [exportForm] = Form.useForm(); const [spotcheckForm] = Form.useForm(); const [reviewForm] = Form.useForm(); const [batchForm] = Form.useForm(); const [dataSource, setDataSource] = useState([]); const [total, setTotal] = useState(0); const [statData, setStatData] = useState({}); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [detailVisible, setDetailVisible] = useState(false); const [detailData, setDetailData] = useState(null); const [aiVisible, setAiVisible] = useState(false); const [aiData, setAiData] = useState(null); const [exportVisible, setExportVisible] = useState(false); const [batchVisible, setBatchVisible] = useState(false); const [spotcheckVisible, setSpotcheckVisible] = useState(false); const [spotcheckList, setSpotcheckList] = useState([]); const [spotcheckStartVisible, setSpotcheckStartVisible] = useState(false); const [reviewVisible, setReviewVisible] = useState(false); const [reviewRecord, setReviewRecord] = useState(null); const [currentReport, setCurrentReport] = useState(null); // 抽检 const [spotCheckVisible, setSpotCheckVisible] = useState(false); const [spotCheckData, setSpotCheckData] = useState([]); const [orgOptions, setOrgOptions] = useState([]); const [spotCheckForm] = Form.useForm(); const [viewChecklistLoading, setViewChecklistLoading] = useState(false); const [spotDetailVisible, setSpotDetailVisible] = useState(false); const [spotDetailData, setSpotDetailData] = useState(null); const { regulatorEvalReportPageLoading, regulatorEvalReportDetailLoading, regulatorSpotcheckListLoading, regulatorSpotcheckStartLoading, regulatorSpotcheckReviewLoading, regulatorSpotCheckReportLoading, } = props.regulatorEvalReport || {}; const { registeredOrgListData } = props.orgInfo || {}; const fetchSummary = async () => { const res = await props.regulatorEvalReportSummary(); if (res?.success !== false) { setStatData(res?.data || {}); } }; const getData = async (pagination) => { const params = { ...router.query, current: pagination?.current || router.query.current || 1, size: pagination?.size || router.query.size || 20, }; // RangePicker 拆成接口字段 if (params.archiveTimeRange?.length === 2) { params.archiveTimeStart = dayjs(params.archiveTimeRange[0]).format( "YYYY-MM-DD", ); params.archiveTimeEnd = dayjs(params.archiveTimeRange[1]).format( "YYYY-MM-DD", ); } delete params.archiveTimeRange; const res = await props.regulatorEvalReportPage(params); if (res?.success !== false) { setDataSource(res?.data || []); setTotal(res?.total || 0); } }; useEffect(() => { const q = { ...router.query }; if (q.archiveTimeStart && q.archiveTimeEnd) { q.archiveTimeRange = [dayjs(q.archiveTimeStart), dayjs(q.archiveTimeEnd)]; } searchForm.setFieldsValue(q); fetchSummary(); getData(); loadOrgOptions(); }, []); const loadOrgOptions = async () => { try { const res = await props.registeredOrgList({ current: 1, size: 200 }); const list = res?.data || []; setOrgOptions( list.map((item) => ({ label: item.unitName || item.orgName || item.name || item.enterpriseName || String(item.id), value: String(item.id), })), ); } catch { setOrgOptions([]); } }; const handleOpenSpotCheck = () => { spotCheckForm.resetFields(); spotCheckForm.setFieldsValue({ randomNumber: 2, checkType: "QUALITY", planCheckDate: dayjs(), }); setSpotCheckData([]); setSpotCheckVisible(true); }; const handleSpotCheck = async () => { const values = await spotCheckForm.validateFields().catch(() => null); if (!values) return; const res = await props.regulatorSpotCheckReport({ orgId: values.orgId, randomNumber: values.randomNumber, }); if (res?.success !== false) { setSpotCheckData(res?.data || []); message.success(`已随机抽取 ${res?.data?.length || 0} 份报告`); setTimeout(() => { document .querySelector(".redb-modal .micro-temp-modal-body") ?.scrollTo({ top: 999999, behavior: "smooth" }); }, 100); } }; const getChecklistData = () => { const data = registeredOrgListData.find( (o) => o.id === spotCheckForm.getFieldValue("orgId"), ); const unitName = data.unitName; return { unitName, fileName: `安全评价报告监督检查单_${unitName}.docx`, data: { unitName, creditCode: data.creditCode || "", qualificationCertNo: data.qualificationCertNo || "", legalRepresentative: data.legalRepresentative || "", businessAddress: data.businessAddress || "", principalName: data.principalName || "", principalPhone: data.principalPhone || "", checkTypeName: getOptionLabel( CHECK_TYPE_OPTIONS, spotCheckForm.getFieldValue("checkType"), ), planCheckDate: spotCheckForm.getFieldValue("planCheckDate") ? dayjs(spotCheckForm.getFieldValue("planCheckDate")).format( "YYYY年M月D日", ) : "", checkLeader: spotCheckForm.getFieldValue("checkLeader") || "", checkLocation: spotCheckForm.getFieldValue("checkLocation") || "", checkMembers: spotCheckForm.getFieldValue("checkMembers") || "", reports: spotCheckData.map((item,index) => ({ index: index + 1, reportNo: item.reportNo || "", reportName: item.reportName || "", evalTypeName: item.evalTypeName || "", industryName: item.industryName || "", issueDate: item.issueDate || "", })), }, }; }; const uploadDocx = async (blob, fileName) => { const formData = new FormData(); formData.append("file", new File([blob], fileName), fileName); const res = await fetch( `${window.process.env.app.API_HOST}/safetyEval/file/upload`, { method: "POST", headers: { token: sessionStorage.getItem("token") }, body: formData, }, ); const json = await res.json(); const url = json?.data?.url; if (!url) throw new Error("上传失败"); return url; }; const handleViewChecklist = async () => { setViewChecklistLoading(true); try { const { fileName, data } = getChecklistData(); const blob = await renderDocxBlob(CHECKLIST_TEMPLATE_URL, data); const url = await uploadDocx(blob, fileName); window.open(url, "_blank"); message.success("检查单已生成"); } finally { setViewChecklistLoading(false); } }; const handleDownloadChecklist = async () => { const { fileName, data } = getChecklistData(); await fillDocxTemplate( CHECKLIST_TEMPLATE_URL, data, fileName, { download: true }, ); message.success("检查单已下载"); }; const handleSearch = (values) => { const next = { ...values, current: 1, size: router.query.size || 20 }; if (next.archiveTimeRange?.length === 2) { next.archiveTimeStart = dayjs(next.archiveTimeRange[0]).format( "YYYY-MM-DD", ); next.archiveTimeEnd = dayjs(next.archiveTimeRange[1]).format( "YYYY-MM-DD", ); } else { next.archiveTimeStart = undefined; next.archiveTimeEnd = undefined; } delete next.archiveTimeRange; router.query = { ...router.query, ...next }; getData(); }; const handleReset = (values) => { searchForm.resetFields(); router.query = { ...values, current: 1, size: 20 }; getData(); }; const handlePageChange = (pagination) => { router.query = { ...router.query, current: pagination.current, size: pagination.pageSize, }; getData(pagination); }; const handleViewDetail = async (record) => { setDetailData(null); setDetailVisible(true); const res = await props.regulatorEvalReportDetail({ id: record.id }); if (res?.success !== false) { setDetailData(res?.data || {}); } }; /** 抽检报告详情:直接用行数据展示,不调接口 */ const handleViewSpotDetail = (record) => { setSpotDetailData(record); setSpotDetailVisible(true); }; const handleDownloadOne = async (record) => { let fileUrl = record.fileUrl; let fileName = record.fileName || record.reportName || "报告文件"; if (!fileUrl) { const res = await props.regulatorEvalReportDetail({ id: record.id }); if (res?.success === false) return; fileUrl = res?.data?.fileUrl; fileName = res?.data?.fileName || fileName; } if (!fileUrl) { message.warning("文件地址不存在"); return; } const hide = message.loading("正在下载...", 0); try { await downloadFileByUrl(fileUrl, fileName); message.success("下载已开始"); } catch { message.error("下载失败,请稍后重试"); } finally { hide(); } }; const handleOpenAi = (record) => { setAiData({ record, analysis: buildStaticAiAnalysis(record) }); setAiVisible(true); }; const handleBatchDownload = () => { if (!selectedRowKeys.length) { message.warning("请先勾选需要下载的报告"); return; } if (selectedRowKeys.length > 50) { message.warning("批量下载上限 50 份"); return; } batchForm.setFieldsValue({ scopeLabel: `批量下载(已选 ${selectedRowKeys.length} 份)`, keepRecord: "是", remark: "", }); setBatchVisible(true); }; const handleBatchOk = async () => { const hide = message.loading("正在打包下载...", 0); try { await downloadBinaryPost( "/safetyEval/regulator/eval-report/batch-download", { ids: selectedRowKeys }, `安评报告批量下载_${dayjs().format("YYYYMMDDHHmmss")}.zip`, ); message.success("批量操作已提交"); setBatchVisible(false); } catch (err) { message.error(err.message || "批量下载失败"); } finally { hide(); } }; const handleOpenExport = () => { exportForm.setFieldsValue({ scope: selectedRowKeys.length ? "SELECTED" : "FILTERED", }); setExportVisible(true); }; const handleExportOk = async () => { const values = await exportForm.validateFields().catch(() => null); if (!values) return; const body = { scope: values.scope, ...router.query, }; delete body.current; delete body.size; delete body.archiveTimeRange; if (values.scope === "SELECTED") { if (!selectedRowKeys.length) { message.warning("请先勾选需要导出的报告"); return; } body.ids = selectedRowKeys; } const hide = message.loading("正在导出目录...", 0); try { await downloadBinaryPost( "/safetyEval/regulator/eval-report/export-catalog", body, `安评报告数据库目录_${dayjs().format("YYYYMMDDHHmmss")}.csv`, ); message.success("目录导出已开始"); setExportVisible(false); } catch (err) { message.error(err.message || "导出失败"); } finally { hide(); } }; const handleOpenSpotcheckStart = (record) => { setCurrentReport(record); spotcheckForm.resetFields(); spotcheckForm.setFieldsValue({ checkResultCode: 1 }); setSpotcheckStartVisible(true); }; const handleSpotcheckStartOk = async () => { const values = await spotcheckForm.validateFields().catch(() => null); if (!values) return; if (values.checkResultCode === 2 && !values.rectifyRequire?.trim()) { message.warning("不合格时必须填写整改要求"); return; } const res = await props.regulatorSpotcheckStart({ reportId: currentReport.id, checkResultCode: values.checkResultCode, checkResultName: values.checkResultCode === 1 ? "合格" : "不合格", checkOpinion: values.checkOpinion, rectifyRequire: values.rectifyRequire, }); if (res?.success !== false) { message.success("抽查已发起"); setSpotcheckStartVisible(false); getData(); if (spotcheckVisible && currentReport?.id) { const listRes = await props.regulatorSpotcheckList({ reportId: currentReport.id, }); if (listRes?.success !== false) setSpotcheckList(listRes?.data || []); } } }; const handleOpenReview = (record) => { setReviewRecord(record); reviewForm.resetFields(); reviewForm.setFieldsValue({ reviewResultCode: 1 }); setReviewVisible(true); }; const handleReviewOk = async () => { const values = await reviewForm.validateFields().catch(() => null); if (!values) return; const res = await props.regulatorSpotcheckReview({ spotcheckId: reviewRecord.id, reviewResultCode: values.reviewResultCode, reviewResultName: values.reviewResultCode === 1 ? "通过" : "不通过", }); if (res?.success !== false) { message.success("复核结果已提交"); setReviewVisible(false); getData(); if (currentReport?.id) { const listRes = await props.regulatorSpotcheckList({ reportId: currentReport.id, }); if (listRes?.success !== false) setSpotcheckList(listRes?.data || []); } } }; const columns = [ { title: "报告编号", dataIndex: "reportNo", width: 150, }, { title: "报告名称", dataIndex: "reportName", width: 240, ellipsis: true, render: (val) => val || "-", }, { title: "报送机构", dataIndex: "orgName", width: 200, ellipsis: true, render: (val) => val || "-", }, { title: "评价类型", dataIndex: "evalTypeName", width: 120, render: (val, record) => val || getOptionLabel(EVAL_TYPE_OPTIONS, record.evalTypeCode), }, { title: "行业", dataIndex: "industryName", width: 100, render: (val, record) => val || getOptionLabel(INDUSTRY_OPTIONS, record.industryCode), }, { title: "签发日期", dataIndex: "issueDate", width: 120, render: (val) => (val ? dayjs(val).format("YYYY-MM-DD") : "-"), }, { title: "报送时间", dataIndex: "archiveTime", width: 160, render: (val) => (val ? dayjs(val).format("YYYY-MM-DD HH:mm") : "-"), }, { title: "操作", width: 200, fixed: "right", render: (_, record) => ( handleViewDetail(record)}>查看报告 handleOpenAi(record)}>AI分析 ), }, ]; const spotcheckColumns = [ { title: "抽查时间", dataIndex: "checkTime", width: 160, render: (val) => (val ? dayjs(val).format("YYYY-MM-DD HH:mm") : "-"), }, { title: "抽查人", dataIndex: "checkerName", width: 100, render: (val) => val || "-", }, { title: "抽查结果", dataIndex: "checkResultName", width: 90, render: (val, record) => { if (record.checkResultCode === 1) return 合格; if (record.checkResultCode === 2) return 不合格; return val || "-"; }, }, { title: "抽查意见", dataIndex: "checkOpinion", ellipsis: true, render: (val) => val || "-", }, { title: "整改要求", dataIndex: "rectifyRequire", ellipsis: true, render: (val) => val || "-", }, { title: "整改反馈", dataIndex: "rectifyFeedback", ellipsis: true, render: (val) => val || "-", }, { title: "复核结果", dataIndex: "reviewResultName", width: 100, render: (val, record) => { if (record.reviewResultCode === 1) return 通过; if (record.reviewResultCode === 2) return 不通过; return val || "-"; }, }, { title: "操作", width: 100, fixed: "right", render: (_, record) => { // 有整改反馈且尚未复核时可复核 if (record.rectifyFeedback && !record.reviewResultCode) { return handleOpenReview(record)}>复核; } return "-"; }, }, ]; const currentYear = dayjs().year(); const levelClass = useMemo( () => ({ 重点: "redb-tag-danger", 一般: "redb-tag-warning", 低: "redb-tag-success", }), [], ); return (
近三年报告
{statData.recentThreeYearCount ?? "-"}份
{currentYear}年度
{statData.currentYearCount ?? "-"}份
{statData.submittedTotalCount != null && (
已报送合计
{statData.submittedTotalCount}份
)}
, , , , , , ]} onFinish={handleSearch} onReset={handleReset} />
共 {total} 条报告记录
{ setSelectedRowKeys(keys); }, }} scroll={{ y: props.scrollY, x: 1400 }} pagination={{ total, current: Number(router.query.current) || 1, pageSize: Number(router.query.size) || 20, showSizeChanger: true, showQuickJumper: true, pageSizeOptions: ["20", "50", "100"], showTotal: (count) => `共 ${count} 条`, }} onChange={handlePageChange} /> {/* 查看报告 — 对齐原型 reportLibraryDetail */} setDetailVisible(false)} width={980} destroyOnHide className="redb-modal" footer={ {detailData && ( )} } > {detailData ? ( <> {detailData.fileName || detailData.reportName || "正式安全评价报告.pdf"} , getFileExtLabel(detailData.fileName), "V1.0", ], ]} /> [ {f.name || "未命名文件"} , getFileExtLabel(f.name || f.url), ])} /> [ {f.name || "未命名文件"} , getFileExtLabel(f.name || f.url), ])} /> ) : (
{regulatorEvalReportDetailLoading ? "加载中..." : "暂无数据"}
)}
{/* AI分析 — 对齐原型 reportAiAnalysis */} setAiVisible(false)} width={980} destroyOnClose className="redb-modal" footer={ } > {aiData && ( <>
{aiData.analysis.metrics.map((m) => ( {m.label}:{m.value} ))}
{aiData.analysis.rows.map((row) => ( ))}
分析维度 AI识别结果 风险等级 建议人工核查内容
{row.dim} {row.result} {row.level} {row.advice}
AI结论: {aiData.analysis.conclusion}
)} {/* 导出目录 — 对齐原型 export */} setExportVisible(false)} onOk={handleExportOk} okText="生成文件" cancelText="关闭" width={720} destroyOnClose className="redb-modal" >