safety-eval-service-frontend/src/pages/Container/Supervision/EvalReportDatabase/index.js

1587 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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);
};
/** OpenAPIAI 分析为前端静态;文案对齐原型 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 }) => (
<div className="redb-modal-section">
<h4>{title}</h4>
{desc ? <p>{desc}</p> : null}
{children}
</div>
);
/** 原型 tableHtml字段表 / 数据表kv=字段-内容两列表 */
const ProtoTable = ({ headers, rows, kv = false }) => (
<div className={`redb-data-table${kv ? " redb-data-table-kv" : ""}`}>
<table>
<thead>
<tr>
{headers.map((h) => (
<th key={h}>{h}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, idx) => (
<tr key={idx}>
{row.map((cell, cIdx) => (
<td key={cIdx}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
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) => (
<TableAction size={4} wrap>
<a onClick={() => handleViewDetail(record)}>查看报告</a>
<a onClick={() => handleOpenAi(record)}>AI分析</a>
</TableAction>
),
},
];
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 <span className="redb-tag redb-tag-success">合格</span>;
if (record.checkResultCode === 2)
return <span className="redb-tag redb-tag-danger">不合格</span>;
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 <span className="redb-tag redb-tag-success">通过</span>;
if (record.reviewResultCode === 2)
return <span className="redb-tag redb-tag-danger">不通过</span>;
return val || "-";
},
},
{
title: "操作",
width: 100,
fixed: "right",
render: (_, record) => {
// 有整改反馈且尚未复核时可复核
if (record.rectifyFeedback && !record.reviewResultCode) {
return <a onClick={() => handleOpenReview(record)}>复核</a>;
}
return "-";
},
},
];
const currentYear = dayjs().year();
const levelClass = useMemo(
() => ({
重点: "redb-tag-danger",
一般: "redb-tag-warning",
: "redb-tag-success",
}),
[],
);
return (
<ConfigProvider theme={PROTO_THEME}>
<PageLayout title="安评报告数据库">
<div className="redb-page">
<div className="redb-summary-cards">
<div className="redb-summary-card">
<div className="label">近三年报告</div>
<div className="value">
<strong>{statData.recentThreeYearCount ?? "-"}</strong>
</div>
</div>
<div className="redb-summary-card">
<div className="label">{currentYear}年度</div>
<div className="value">
<strong>{statData.currentYearCount ?? "-"}</strong>
</div>
</div>
{statData.submittedTotalCount != null && (
<div className="redb-summary-card">
<div className="label">已报送合计</div>
<div className="value">
<strong>{statData.submittedTotalCount}</strong>
</div>
</div>
)}
</div>
<SearchForm
form={searchForm}
loading={false}
style={{ marginBottom: 12 }}
formLine={[
<Form.Item key="keyword" name="keyword">
<ControlWrapper.Input
label="报告名称/编号"
placeholder="输入报告名称或编号"
allowClear
/>
</Form.Item>,
<Form.Item key="orgName" name="orgName">
<ControlWrapper.Input
label="报送机构"
placeholder="输入评价机构名称"
allowClear
/>
</Form.Item>,
<Form.Item key="reportYear" name="reportYear">
<ControlWrapper.Select
label="报告年度"
placeholder="全部"
allowClear
options={getYearOptions()}
/>
</Form.Item>,
<Form.Item key="evalTypeCode" name="evalTypeCode">
<ControlWrapper.Select
label="评价类型"
placeholder="全部"
allowClear
options={EVAL_TYPE_OPTIONS}
/>
</Form.Item>,
<Form.Item key="industryCode" name="industryCode">
<ControlWrapper.Select
label="行业类别"
placeholder="全部"
allowClear
options={INDUSTRY_OPTIONS}
/>
</Form.Item>,
<Form.Item key="archiveTimeRange" name="archiveTimeRange">
<RangePicker
style={{ width: "100%" }}
placeholder={["报送时间起", "报送时间止"]}
/>
</Form.Item>,
]}
onFinish={handleSearch}
onReset={handleReset}
/>
<div className="redb-toolbar">
<div className="redb-toolbar-left">
<span className="redb-total"> {total} 条报告记录</span>
</div>
<div className="redb-toolbar-right">
<Button type="primary" size="small" onClick={handleOpenSpotCheck}>
抽检
</Button>
<Button
size="small"
icon={<DownloadOutlined />}
onClick={handleBatchDownload}
>
批量下载
</Button>
<Button
size="small"
icon={<ExportOutlined />}
onClick={handleOpenExport}
>
导出目录
</Button>
</div>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={regulatorEvalReportPageLoading}
rowSelection={{
selectedRowKeys,
onChange: (keys) => {
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}
/>
</div>
{/* 查看报告 — 对齐原型 reportLibraryDetail */}
<Modal
title={
detailData
? `查看安全评价报告 - ${detailData.reportNo || ""}`
: "查看安全评价报告"
}
open={detailVisible}
onCancel={() => setDetailVisible(false)}
width={980}
destroyOnHide
className="redb-modal"
footer={
<Space>
<Button onClick={() => setDetailVisible(false)}>关闭</Button>
{detailData && (
<Button
type="primary"
onClick={() => handleDownloadOne(detailData)}
disabled={!detailData.fileUrl && !detailData.id}
>
下载报告
</Button>
)}
</Space>
}
>
{detailData ? (
<>
<ModalSection
title="报告基本信息"
desc="展示评价机构上传的正式安全评价报告信息。"
>
<ProtoTable
kv
headers={["字段", "内容"]}
rows={[
["报告编号", detailData.reportNo || "-"],
["报告名称", detailData.reportName || "-"],
["评价机构", detailData.orgName || "-"],
[
"评价类型",
detailData.evalTypeName ||
getOptionLabel(
EVAL_TYPE_OPTIONS,
detailData.evalTypeCode,
),
],
[
"所属行业",
detailData.industryName ||
getOptionLabel(
INDUSTRY_OPTIONS,
detailData.industryCode,
),
],
[
"签发日期",
detailData.issueDate
? dayjs(detailData.issueDate).format("YYYY-MM-DD")
: "-",
],
[
"报送时间",
detailData.archiveTime
? dayjs(detailData.archiveTime).format(
"YYYY-MM-DD HH:mm",
)
: "-",
],
]}
/>
</ModalSection>
<ModalSection
title="报告文件"
desc="支持在线查看和下载正式报告。"
>
<ProtoTable
headers={["文件名称", "文件类型", "版本"]}
rows={[
[
<PreviewUrlButton
url={detailData.fileUrl}
>
{detailData.fileName ||
detailData.reportName ||
"正式安全评价报告.pdf"}
</PreviewUrlButton>,
getFileExtLabel(detailData.fileName),
"V1.0",
],
]}
/>
</ModalSection>
<ModalSection title="合同文件" desc="查看上传的合同文件。">
<ProtoTable
headers={["文件名称", "文件类型"]}
rows={parseFileList(detailData.contractFileUrls).map((f) => [
<PreviewUrlButton key={f.url} url={f.url}>
{f.name || "未命名文件"}
</PreviewUrlButton>,
getFileExtLabel(f.name || f.url),
])}
/>
</ModalSection>
<ModalSection title="其他过程文档" desc="查看上传的过程文档。">
<ProtoTable
headers={["文件名称", "文件类型"]}
rows={parseFileList(detailData.processDocUrls).map((f) => [
<PreviewUrlButton key={f.url} url={f.url}>
{f.name || "未命名文件"}
</PreviewUrlButton>,
getFileExtLabel(f.name || f.url),
])}
/>
</ModalSection>
</>
) : (
<div className="redb-loading">
{regulatorEvalReportDetailLoading ? "加载中..." : "暂无数据"}
</div>
)}
</Modal>
{/* AI分析 — 对齐原型 reportAiAnalysis */}
<Modal
title={
aiData
? `AI报告分析 - ${aiData.record?.reportNo || aiData.record?.reportName || ""}`
: "AI报告分析"
}
open={aiVisible}
onCancel={() => setAiVisible(false)}
width={980}
destroyOnClose
className="redb-modal"
footer={
<Space>
<Button onClick={() => setAiVisible(false)}>关闭</Button>
<Button
type="primary"
onClick={() => {
message.success("AI分析结果文件已生成");
setAiVisible(false);
}}
>
生成AI分析报告
</Button>
</Space>
}
>
{aiData && (
<>
<ModalSection title="分析概览" desc={aiData.analysis.overview}>
<div
className="redb-summary-strip"
style={{ marginTop: "11.2px", marginBottom: 0 }}
>
{aiData.analysis.metrics.map((m) => (
<span key={m.label}>
{m.label}<strong>{m.value}</strong>
</span>
))}
</div>
</ModalSection>
<div className="redb-data-table">
<table>
<thead>
<tr>
<th>分析维度</th>
<th>AI识别结果</th>
<th>风险等级</th>
<th>建议人工核查内容</th>
</tr>
</thead>
<tbody>
{aiData.analysis.rows.map((row) => (
<tr key={row.dim}>
<td>{row.dim}</td>
<td>{row.result}</td>
<td>
<span
className={`redb-tag ${levelClass[row.level] || ""}`}
>
{row.level}
</span>
</td>
<td>{row.advice}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="redb-ai-note">
<strong>AI结论</strong>
{aiData.analysis.conclusion}
</div>
</>
)}
</Modal>
{/* 导出目录 — 对齐原型 export */}
<Modal
title="安评报告数据库目录"
open={exportVisible}
onCancel={() => setExportVisible(false)}
onOk={handleExportOk}
okText="生成文件"
cancelText="关闭"
width={720}
destroyOnClose
className="redb-modal"
>
<ModalSection
title="导出设置"
desc="导出当前查询范围内的数据,系统将记录导出人和导出时间。"
>
<Form
form={exportForm}
layout="vertical"
preserve={false}
className="redb-form-grid"
scrollToFirstError
>
<Form.Item
name="scope"
label="导出范围"
rules={[{ required: true, message: "请选择导出范围" }]}
className="redb-field"
>
<Select
options={[
{ label: "当前勾选", value: "SELECTED" },
{ label: "当前查询结果", value: "FILTERED" },
{ label: "全部数据", value: "ALL" },
]}
/>
</Form.Item>
<Form.Item
name="fileFormat"
label="文件格式"
initialValue="CSV"
className="redb-field"
>
<Select
options={[
{ label: "CSV", value: "CSV" },
{ label: "Excel", value: "Excel" },
{ label: "PDF", value: "PDF" },
]}
/>
</Form.Item>
<Form.Item
name="exportRemark"
label="导出说明"
className="redb-field redb-field-full"
>
<TextArea rows={3} placeholder="选填" />
</Form.Item>
</Form>
</ModalSection>
</Modal>
{/* 批量下载 — 对齐原型 reportBatchAction */}
<Modal
title="批量下载"
open={batchVisible}
onCancel={() => setBatchVisible(false)}
onOk={handleBatchOk}
okText="确认执行"
cancelText="关闭"
width={720}
destroyOnClose
className="redb-modal"
>
<ModalSection
title="批量操作确认"
desc="批量分类将按年度、行业和评价类型重新校验目录;批量下载将生成所选报告的压缩文件。"
>
<Form
form={batchForm}
layout="vertical"
preserve={false}
className="redb-form-grid"
scrollToFirstError
>
<Form.Item
name="scopeLabel"
label="操作范围"
className="redb-field"
>
<Input disabled />
</Form.Item>
<Form.Item
name="keepRecord"
label="保留原分类记录"
className="redb-field"
>
<Select
options={[
{ label: "是", value: "是" },
{ label: "否", value: "否" },
]}
/>
</Form.Item>
<Form.Item
name="remark"
label="操作说明"
className="redb-field redb-field-full"
>
<TextArea rows={3} placeholder="选填" />
</Form.Item>
</Form>
</ModalSection>
</Modal>
{/* 抽查记录 */}
<Modal
title={
currentReport
? `抽查记录 — ${currentReport.reportName || currentReport.reportNo || ""}`
: "抽查记录"
}
open={spotcheckVisible}
onCancel={() => setSpotcheckVisible(false)}
width={980}
destroyOnClose
className="redb-modal"
footer={
<Space>
<Button onClick={() => setSpotcheckVisible(false)}>关闭</Button>
{currentReport?.reportStatusCode === 1 && (
<Button
type="primary"
onClick={() => handleOpenSpotcheckStart(currentReport)}
>
发起抽查
</Button>
)}
</Space>
}
>
<Table
rowKey="id"
size="small"
columns={spotcheckColumns}
dataSource={spotcheckList}
loading={regulatorSpotcheckListLoading}
pagination={false}
scroll={{ x: 1100 }}
locale={{ emptyText: "暂无抽查记录" }}
/>
</Modal>
{/* 发起抽查 */}
<Modal
title={
currentReport
? `发起报告抽查 - ${currentReport.reportNo || ""}`
: "发起报告抽查"
}
open={spotcheckStartVisible}
onCancel={() => setSpotcheckStartVisible(false)}
onOk={handleSpotcheckStartOk}
confirmLoading={regulatorSpotcheckStartLoading}
okText="提交抽查"
width={640}
destroyOnClose
className="redb-modal"
>
<Form
form={spotcheckForm}
layout="vertical"
preserve={false}
scrollToFirstError
>
<Form.Item
name="checkResultCode"
label="抽查结果"
rules={[{ required: true, message: "请选择抽查结果" }]}
>
<Select
options={[
{ label: "合格", value: 1 },
{ label: "不合格", value: 2 },
]}
/>
</Form.Item>
<Form.Item name="checkOpinion" label="抽查意见">
<TextArea
rows={3}
placeholder="填写抽查意见"
maxLength={500}
showCount
/>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.checkResultCode !== cur.checkResultCode
}
>
{({ getFieldValue }) =>
getFieldValue("checkResultCode") === 2 ? (
<Form.Item
name="rectifyRequire"
label="整改要求"
rules={[
{ required: true, message: "不合格须填写整改要求" },
]}
>
<TextArea
rows={3}
placeholder="请填写整改要求"
maxLength={500}
showCount
/>
</Form.Item>
) : null
}
</Form.Item>
</Form>
</Modal>
{/* 复核 */}
<Modal
title="抽查复核"
open={reviewVisible}
onCancel={() => setReviewVisible(false)}
onOk={handleReviewOk}
confirmLoading={regulatorSpotcheckReviewLoading}
okText="提交复核结果"
width={520}
destroyOnClose
className="redb-modal"
>
{reviewRecord && (
<div className="redb-note" style={{ marginBottom: 12 }}>
<div>整改反馈{reviewRecord.rectifyFeedback || "-"}</div>
</div>
)}
<Form
form={reviewForm}
layout="vertical"
preserve={false}
scrollToFirstError
>
<Form.Item
name="reviewResultCode"
label="复核结果"
rules={[{ required: true, message: "请选择复核结果" }]}
>
<Select
options={[
{ label: "通过", value: 1 },
{ label: "不通过", value: 2 },
]}
/>
</Form.Item>
</Form>
</Modal>
{/* 抽检 - 随机抽取并生成检查单 */}
<Modal
title="安评报告随机抽检"
open={spotCheckVisible}
onCancel={() => setSpotCheckVisible(false)}
footer={
<Space>
<Button onClick={() => setSpotCheckVisible(false)}>关闭</Button>
<Button
type="primary"
loading={regulatorSpotCheckReportLoading}
onClick={handleSpotCheck}
>
随机抽取并生成检查单
</Button>
</Space>
}
width={980}
destroyOnHidden
className="redb-modal"
>
<div className="redb-modal-section">
<h4>抽检设置</h4>
<p>
从选定评价机构已报送的安全评价报告中随机抽取生成监督检查单
</p>
</div>
<Form form={spotCheckForm} layout="vertical" preserve={false}>
<Row gutter={16}>
<Col span={24}>
<Form.Item
name="orgId"
label="评价机构"
rules={[{ required: true, message: "请选择评价机构" }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder="请选择评价机构"
options={orgOptions}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="randomNumber"
label="检查份数"
rules={[{ required: true, message: "请输入检查份数" }]}
>
<InputNumber min={1} max={10} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="checkType" label="检查类型">
<Select options={CHECK_TYPE_OPTIONS} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="planCheckDate" label="计划检查日期">
<DatePicker
style={{ width: "100%" }}
placeholder="选择日期"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="checkLeader" label="检查负责人">
<Input placeholder="请输入检查负责人" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="checkLocation" label="检查地点">
<Input placeholder="请输入检查地点" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="checkMembers" label="检查组成员">
<Input placeholder="请输入检查组成员" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="checkBasis" label="补充检查依据">
<TextArea
rows={2}
placeholder="可填写本次专项检查、年度计划或属地监管文件依据"
/>
</Form.Item>
</Col>
</Row>
</Form>
{spotCheckData.length > 0 && (
<>
<div className="redb-modal-section">
<h4>随机抽取结果</h4>
<p>
本次从"
{orgOptions.find(
(o) => o.value === spotCheckForm.getFieldValue("orgId"),
)?.label || ""}
"报告库中随机抽取 {spotCheckData.length}{" "}
抽取结果已写入检查单
</p>
</div>
<div className="redb-modal-section">
<div className="redb-spotcheck-header">
<div>
<h4 style={{ margin: 0 }}>随机抽取报告</h4>
</div>
<Space>
<Button
size="small"
type="primary"
loading={viewChecklistLoading}
onClick={handleViewChecklist}
>
查看检查单
</Button>
<Button
size="small"
onClick={handleDownloadChecklist}
>
下载检查单
</Button>
<Button
size="small"
loading={regulatorSpotCheckReportLoading}
onClick={handleSpotCheck}
>
重新抽取
</Button>
</Space>
</div>
<Table
rowKey="archiveTime"
size="small"
columns={[
{
title: "报告名称",
dataIndex: "reportName",
ellipsis: true,
},
{
title: "被检查机构",
dataIndex: "orgName",
width: 200,
ellipsis: true,
},
{
title: "操作",
width: 90,
render: (_, record) => (
<Button
type="primary"
size="small"
onClick={() => handleViewSpotDetail(record)}
>
查看
</Button>
),
},
]}
dataSource={spotCheckData}
pagination={false}
/>
</div>
</>
)}
</Modal>
<SpotCheckDetailModal
open={spotDetailVisible}
record={spotDetailData}
onCancel={() => setSpotDetailVisible(false)}
/>
</PageLayout>
</ConfigProvider>
);
};
export default Connect(
[NS_REGULATOR_EVAL_REPORT, NS_ORG_INFO],
true,
)(AntdTableFuncControl(EvalReportDatabase));