import React, { useState, useEffect } from "react";
import {
Form,
Table,
Button,
Modal,
Input,
Select,
InputNumber,
Row,
Col,
message,
Descriptions,
Upload,
DatePicker,
Space,
ConfigProvider,
Tag,
Tooltip,
} from "antd";
import {
PlusOutlined,
UploadOutlined,
SendOutlined,
} 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_SAFETY_EVAL_BUSINESS } 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 UPLOAD_ACTION = `${window.process?.env?.app?.API_HOST || ""}/safetyEval/file/upload`;
/** 评价类型(对齐原型文案;编码对齐 OpenAPI PRE/ACCEPT/STATUS) */
const REPORT_EVAL_TYPE_OPTIONS = [
{ label: "安全预评价", value: "PRE" },
{ label: "安全验收评价", value: "ACCEPT" },
{ label: "安全现状评价", value: "STATUS" },
];
/** 所属行业(原型选项;编码仅 OpenAPI 示例给出 HAZCHEM,其余按命名约定) */
const REPORT_INDUSTRY_OPTIONS = [
{ label: "危险化学品", value: "HAZCHEM" },
{ label: "金属非金属矿山", value: "METAL_NONMETAL_MINE" },
{ label: "金属冶炼", value: "METAL_SMELTING" },
{ label: "烟花爆竹", value: "FIREWORKS" },
{ label: "城镇燃气", value: "URBAN_GAS" },
{ label: "工贸", value: "INDUSTRY_TRADE" },
{ label: "其他", value: "OTHER" },
];
/** 保密属性(OpenAPI 仅示例 NORMAL) */
const SECRET_LEVEL_OPTIONS = [
{ label: "普通", value: "NORMAL" },
{ label: "含商业秘密", value: "TRADE_SECRET" },
{ label: "涉个人隐私", value: "PERSONAL_PRIVACY" },
{ label: "涉密资料", value: "CLASSIFIED" },
];
/** 报送状态筛选(对齐原型 + OpenAPI displayStatus) */
const DISPLAY_STATUS_OPTIONS = [
{ label: "未报送", value: "UN_SUBMITTED" },
{ label: "已报送", value: "SUBMITTED" },
{ label: "抽查中", value: "SPOTCHECKING" },
{ label: "整改中", value: "RECTIFYING" },
];
/** 对齐原型 .tag / .tag-*(胶囊圆角 + 色值) */
const DISPLAY_STATUS_CLASS = {
未报送: "erl-tag-warning",
已报送: "erl-tag-success",
抽查中: "erl-tag-info",
整改中: "erl-tag-danger",
已复核: "erl-tag-success",
};
/** 原型 CSS 变量:--radius 8px / --radius-lg 12px */
const PROTO_THEME = {
token: {
borderRadius: 8,
borderRadiusLG: 12,
borderRadiusSM: 6,
},
};
const getYearOptions = () => {
const year = dayjs().year();
return [0, 1, 2, 3].map((offset) => {
const y = year - offset;
return { label: String(y), value: y };
});
};
const getFileExt = (name = "") => {
const parts = String(name).split(".");
return parts.length > 1 ? parts.pop().toUpperCase() : "PDF";
};
/** 解析 JSON 文件列表,渲染为可下载链接列表 */
const renderFileLinks = (jsonStr) => {
if (!jsonStr) return "-";
try {
const files = JSON.parse(jsonStr);
if (!Array.isArray(files) || files.length === 0) return "-";
return files.map((f, i) => (
));
} catch {
return jsonStr;
}
};
const getOptionLabel = (options, value) =>
options.find((item) => item.value === value)?.label || value || "-";
const isUnsubmitted = (record) =>
record?.submitFlag === 2 ||
record?.displayStatusName === "未报送" ||
record?.displayStatus === "UN_SUBMITTED";
const isSpotcheckRelated = (record) => {
const name = record?.displayStatusName || "";
return ["抽查中", "整改中", "已复核"].includes(name);
};
const EvalReportLibrary = (props) => {
const [searchForm] = Form.useForm();
const [uploadForm] = Form.useForm();
const [rectifyForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [statData, setStatData] = useState({});
const [addVisible, setAddVisible] = useState(false);
const [detailVisible, setDetailVisible] = useState(false);
const [detailData, setDetailData] = useState(null);
const [spotcheckVisible, setSpotcheckVisible] = useState(false);
const [spotcheckData, setSpotcheckData] = useState([]);
const [currentReport, setCurrentReport] = useState(null);
const [rectifyVisible, setRectifyVisible] = useState(false);
const [rectifyRecord, setRectifyRecord] = useState(null);
const [uploadedFile, setUploadedFile] = useState(null);
const [contractFiles, setContractFiles] = useState([]);
const [processDocFiles, setProcessDocFiles] = useState([]);
const {
evalReportPageLoading,
evalReportAddLoading,
evalReportDetailLoading,
evalReportSpotcheckListLoading,
evalReportSpotcheckRectifyLoading,
} = props.safetyEvalBusiness;
const fetchSummary = async () => {
const res = await props.evalReportSummary();
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 || 10,
};
const res = await props.evalReportPage(params);
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
}
};
useEffect(() => {
searchForm.setFieldsValue(router.query);
fetchSummary();
getData();
}, []);
const handleSearch = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
getData();
};
const handleReset = (values) => {
searchForm.resetFields();
router.query = { ...values, current: 1, size: 10 };
getData();
};
const handlePageChange = (pagination) => {
router.query = {
...router.query,
current: pagination.current,
size: pagination.pageSize,
};
getData(pagination);
};
const handleOpenAdd = () => {
setUploadedFile(null);
setContractFiles([]);
setProcessDocFiles([]);
uploadForm.resetFields();
uploadForm.setFieldsValue({
reportYear: dayjs().year(),
evalTypeCode: "PRE",
industryCode: "HAZCHEM",
secretLevelCode: "NORMAL",
issueDate: dayjs(),
submitToRegulator: 0,
});
setAddVisible(true);
};
const handleFileUploadChange = (info) => {
if (info.file.status === "uploading") return;
if (info.file.status === "done") {
const responseData = info.file.response?.data;
const fileUrl = responseData?.url || responseData?.fileUrl || "";
const fileName =
responseData?.name || responseData?.fileName || info.file.name;
if (!fileUrl) {
message.error("上传响应中未获取到文件地址");
return;
}
setUploadedFile({ url: fileUrl, name: fileName });
uploadForm.setFieldsValue({
fileUrl,
fileName,
reportName:
uploadForm.getFieldValue("reportName") ||
fileName.replace(/\.[^.]+$/, ""),
});
message.success(`${fileName} 上传成功`);
} else if (info.file.status === "error") {
message.error(`${info.file.name} 上传失败`);
}
};
// 合同文件上传
const handleContractUploadChange = (info) => {
if (info.file.status === "error") {
message.error(`${info.file.name} 上传失败`);
return;
}
const doneFiles = info.fileList
.filter((f) => f.status === "done" && f.response?.data)
.map((f) => ({
uid: f.uid,
url: f.response.data.url || f.response.data.fileUrl || "",
name: f.response.data.name || f.response.data.fileName || f.name,
}))
.filter((f) => f.url);
setContractFiles(doneFiles);
};
// 其他过程文档上传
const handleProcessDocUploadChange = (info) => {
if (info.file.status === "error") {
message.error(`${info.file.name} 上传失败`);
return;
}
const doneFiles = info.fileList
.filter((f) => f.status === "done" && f.response?.data)
.map((f) => ({
uid: f.uid,
url: f.response.data.url || f.response.data.fileUrl || "",
name: f.response.data.name || f.response.data.fileName || f.name,
}))
.filter((f) => f.url);
setProcessDocFiles(doneFiles);
};
const handleAddOk = async () => {
const values = await uploadForm.validateFields().catch(() => null);
if (!values) return;
if (!values.fileUrl) {
message.warning("请选择需要上传的正式报告文件");
return;
}
const payload = {
reportNo: values.reportNo?.trim(),
reportName: values.reportName?.trim(),
reportYear: values.reportYear,
evalTypeCode: values.evalTypeCode,
evalTypeName: getOptionLabel(REPORT_EVAL_TYPE_OPTIONS, values.evalTypeCode),
industryCode: values.industryCode,
industryName: getOptionLabel(REPORT_INDUSTRY_OPTIONS, values.industryCode),
evaluatedUnitName: values.evaluatedUnitName?.trim(),
issueDate: values.issueDate
? dayjs(values.issueDate).format("YYYY-MM-DD")
: undefined,
pageCount: values.pageCount,
fileUrl: values.fileUrl,
fileName: values.fileName,
secretLevelCode: values.secretLevelCode,
secretLevelName: getOptionLabel(
SECRET_LEVEL_OPTIONS,
values.secretLevelCode,
),
contractFileUrls: contractFiles.length > 0 ? JSON.stringify(contractFiles) : undefined,
processDocUrls: processDocFiles.length > 0 ? JSON.stringify(processDocFiles) : undefined,
remarks: values.remarks,
submitToRegulator: values.submitToRegulator === 1,
};
const res = await props.evalReportAdd(payload);
if (res?.success !== false) {
message.success(
payload.submitToRegulator
? "历史报告已上传并同步报送监管端"
: "历史报告已上传到机构报告库",
);
setAddVisible(false);
fetchSummary();
getData();
}
};
const handleViewDetail = async (record) => {
setDetailData(null);
setDetailVisible(true);
const res = await props.evalReportDetail({ id: record.id });
if (res?.success !== false) {
setDetailData(res?.data || {});
}
};
const handleDownload = async (record) => {
let fileUrl = record.fileUrl;
let fileName = record.fileName || record.reportName || "报告文件";
if (!fileUrl) {
const res = await props.evalReportDetail({ id: record.id });
if (res?.success === false) return;
fileUrl = res?.data?.fileUrl;
fileName = res?.data?.fileName || fileName;
}
if (!fileUrl) {
message.warning("文件地址不存在");
return;
}
// 跨域时 会被忽略并在线打开,改为拉取 blob 再触发本地下载
const hide = message.loading("正在下载...", 0);
try {
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);
message.success("下载已开始");
} catch (err) {
message.error("下载失败,请稍后重试");
} finally {
hide();
}
};
const handleSubmitToRegulator = (record) => {
Modal.confirm({
title: "确认报送",
icon: ,
content: (
确定要将报告「{record.reportName}」报送至监管端吗?报送后监管端可进行抽查审核。
),
okText: "确认报送",
cancelText: "取消",
onOk: async () => {
const res = await props.evalReportSubmit({ id: record.id });
if (res?.success !== false) {
message.success("报告已报送监管端报告数据库");
fetchSummary();
getData();
}
},
});
};
const handleViewSpotcheck = async (record) => {
setCurrentReport(record);
setSpotcheckVisible(true);
setSpotcheckData([]);
const res = await props.evalReportSpotcheckList({ reportId: record.id });
if (res?.success !== false) {
setSpotcheckData(res?.data || []);
}
};
const handleOpenRectify = (record) => {
setRectifyRecord(record);
rectifyForm.resetFields();
setRectifyVisible(true);
};
const handleRectifyOk = async () => {
const values = await rectifyForm.validateFields().catch(() => null);
if (!values) return;
const res = await props.evalReportSpotcheckRectify({
spotcheckId: rectifyRecord.id,
rectifyFeedback: values.rectifyFeedback,
});
if (res?.success !== false) {
message.success("整改反馈提交成功");
setRectifyVisible(false);
if (currentReport?.id) {
const listRes = await props.evalReportSpotcheckList({
reportId: currentReport.id,
});
if (listRes?.success !== false) {
setSpotcheckData(listRes?.data || []);
}
}
fetchSummary();
getData();
}
};
const renderDisplayStatus = (record) => {
const name = record.displayStatusName || record.reportStatusName || "-";
const cls = DISPLAY_STATUS_CLASS[name] || "";
const tag = {name};
if (isSpotcheckRelated(record)) {
return (
handleViewSpotcheck(record)} className="erl-status-link">
{tag}
);
}
return tag;
};
const columns = [
{
title: "报告编号",
dataIndex: "reportNo",
width: 160,
render: (val) => (
{val || "-"}
),
},
{
title: "报告名称",
dataIndex: "reportName",
width: 260,
ellipsis: true,
render: (text, record) => (
),
},
{
title: "年度",
dataIndex: "reportYear",
width: 80,
render: (val) => val || "-",
},
{
title: "评价类型",
dataIndex: "evalTypeName",
width: 120,
render: (val, record) =>
val || getOptionLabel(REPORT_EVAL_TYPE_OPTIONS, record.evalTypeCode),
},
{
title: "所属行业",
dataIndex: "industryName",
width: 120,
render: (val, record) =>
val || getOptionLabel(REPORT_INDUSTRY_OPTIONS, record.industryCode),
},
{
title: "被评价单位",
dataIndex: "evaluatedUnitName",
width: 180,
ellipsis: true,
render: (val) => val || "-",
},
{
title: "签发日期",
dataIndex: "issueDate",
width: 120,
render: (val) => (val ? dayjs(val).format("YYYY-MM-DD") : "-"),
},
{
title: "报送状态",
dataIndex: "displayStatusName",
width: 110,
render: (_, record) => renderDisplayStatus(record),
},
{
title: "操作",
width: 210,
fixed: "right",
render: (_, record) => (
{isUnsubmitted(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: 100,
render: (val, record) => {
if (record.checkResultCode === 1) {
return 合格;
}
if (record.checkResultCode === 2) {
return 不合格;
}
return {val || "-"};
},
},
{
title: "抽查意见",
dataIndex: "checkOpinion",
width: 200,
ellipsis: true,
render: (val) => val || "-",
},
{
title: "整改要求",
dataIndex: "rectifyRequire",
width: 180,
ellipsis: true,
render: (val) => val || "-",
},
{
title: "整改反馈",
dataIndex: "rectifyFeedback",
width: 180,
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: 110,
fixed: "right",
render: (_, record) => {
const needRectify =
record.checkResultCode === 2 && !record.rectifyFeedback;
if (!needRectify) return "-";
return (
);
},
},
];
const summaryItems = [
{ key: "totalCount", label: "报告总数", value: statData.totalCount },
{
key: "recentThreeYearCount",
label: "近三年报告",
value: statData.recentThreeYearCount,
},
{
key: "submittedCount",
label: "已报送监管",
value: statData.submittedCount,
color: "#059669",
},
{
key: "incompleteCount",
label: "待完善分类",
value: statData.incompleteCount,
color: "#d97706",
},
{
key: "spotcheckingCount",
label: "监管抽查中",
value: statData.spotcheckingCount,
color: "#2563eb",
},
];
return (
} onClick={handleOpenAdd}>
上传历史报告
}
>
上传过去三年已执行项目的正式报告,按年度、评价类型、行业和企业分类管理,并可报送监管端抽查。
{summaryItems.map((item) => (
{item.label}
{item.value ?? "-"}
))}
,
,
,
,
,
]}
onFinish={handleSearch}
onReset={handleReset}
/>