import React, { useEffect, useMemo, useState } from "react"; import { Form, Table, Button, Modal, Input, Select, message, DatePicker, Space, ConfigProvider, } from "antd"; import { DownloadOutlined, ExportOutlined, } from "@ant-design/icons"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; import { NS_REGULATOR_EVAL_REPORT } from "~/enumerate/namespace"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { tools } from "@cqsjjb/jjb-common-lib"; 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 || ""; /** 原型 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 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"; }; 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 { regulatorEvalReportPageLoading, regulatorEvalReportDetailLoading, regulatorSpotcheckListLoading, regulatorSpotcheckStartLoading, regulatorSpotcheckReviewLoading, } = props.regulatorEvalReport || {}; 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(); }, []); 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 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 handleOpenSpotcheckList = async (record) => { setCurrentReport(record); setSpotcheckVisible(true); setSpotcheckList([]); const res = await props.regulatorSpotcheckList({ reportId: record.id }); if (res?.success !== false) { setSpotcheckList(res?.data || []); } }; 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, render: (val) => {val || "-"}, }, { 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) => ( ), }, ]; 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 ( ); } 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} destroyOnClose className="redb-modal" footer={ {detailData && ( )} } > {detailData ? ( <> ) : (
{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" >