safety-eval-service-frontend/src/pages/Container/SafetyEvalBusiness/EvalReportLibrary/index.js

1149 lines
36 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, { 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) => (
<div key={i} className="erl-file-cell" style={{ marginBottom: 4 }}>
<i>{getFileExt(f.name || f.url)}</i>
<a
href={f.url}
download={f.name}
target="_blank"
rel="noopener noreferrer"
>
{f.name || "未命名文件"}
</a>
</div>
));
} 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;
}
// 跨域时 <a download> 会被忽略并在线打开,改为拉取 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: <SendOutlined style={{ color: "#1677ff" }} />,
content: (
<div style={{ fontSize: 14, color: "#333", lineHeight: 1.8, padding: "8px 0" }}>
确定要将报告{record.reportName}报送至监管端吗报送后监管端可进行抽查审核
</div>
),
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 = <span className={`erl-tag ${cls}`}>{name}</span>;
if (isSpotcheckRelated(record)) {
return (
<a onClick={() => handleViewSpotcheck(record)} className="erl-status-link">
{tag}
</a>
);
}
return tag;
};
const columns = [
{
title: "报告编号",
dataIndex: "reportNo",
width: 160,
render: (val) => (
<span className="erl-report-no">{val || "-"}</span>
),
},
{
title: "报告名称",
dataIndex: "reportName",
width: 260,
ellipsis: true,
render: (text, record) => (
<div className="erl-file-cell">
<i>{getFileExt(record.fileName || record.fileUrl || text)}</i>
<a onClick={() => handleViewDetail(record)} title={text}>
{text || "-"}
</a>
</div>
),
},
{
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) => (
<Space size={4} wrap className="erl-action-btns">
<Button type="primary" size="small" onClick={() => handleViewDetail(record)}>
查看
</Button>
<Button size="small" onClick={() => handleDownload(record)}>
下载
</Button>
{isUnsubmitted(record) && (
<Button
type="primary"
size="small"
ghost
onClick={() => handleSubmitToRegulator(record)}
>
报送
</Button>
)}
</Space>
),
},
];
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 <span className="erl-tag erl-tag-success">合格</span>;
}
if (record.checkResultCode === 2) {
return <span className="erl-tag erl-tag-danger">不合格</span>;
}
return <span className="erl-tag">{val || "-"}</span>;
},
},
{
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 <span className="erl-tag erl-tag-success">通过</span>;
}
if (record.reviewResultCode === 2) {
return <span className="erl-tag erl-tag-danger">不通过</span>;
}
return val || "-";
},
},
{
title: "操作",
width: 110,
fixed: "right",
render: (_, record) => {
const needRectify =
record.checkResultCode === 2 && !record.rectifyFeedback;
if (!needRectify) return "-";
return (
<Button type="link" size="small" onClick={() => handleOpenRectify(record)}>
提交整改
</Button>
);
},
},
];
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 (
<ConfigProvider theme={PROTO_THEME}>
<PageLayout
title="报告库管理"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenAdd}>
上传历史报告
</Button>
}
>
<div className="erl-page">
<div className="erl-page-desc">
上传过去三年已执行项目的正式报告按年度评价类型行业和企业分类管理并可报送监管端抽查
</div>
<div className="erl-summary">
{summaryItems.map((item) => (
<div key={item.key} className="erl-summary-item">
<span>{item.label}</span>
<strong style={item.color ? { color: item.color } : undefined}>
{item.value ?? "-"}
</strong>
</div>
))}
</div>
<SearchForm
form={searchForm}
loading={false}
style={{ marginBottom: 16 }}
formLine={[
<Form.Item key="keyword" name="keyword">
<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={REPORT_EVAL_TYPE_OPTIONS}
/>
</Form.Item>,
<Form.Item key="industryCode" name="industryCode">
<ControlWrapper.Select
label="所属行业"
placeholder="全部"
allowClear
options={REPORT_INDUSTRY_OPTIONS}
/>
</Form.Item>,
<Form.Item key="displayStatus" name="displayStatus">
<ControlWrapper.Select
label="报送状态"
placeholder="全部"
allowClear
options={DISPLAY_STATUS_OPTIONS}
/>
</Form.Item>,
]}
onFinish={handleSearch}
onReset={handleReset}
/>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={evalReportPageLoading}
scroll={{ y: props.scrollY, x: 1400 }}
pagination={{
total,
current: Number(router.query.current) || 1,
pageSize: Number(router.query.size) || 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (count) => `${count}`,
}}
onChange={handlePageChange}
/>
</div>
{/* 上传历史报告 */}
<Modal
title={
<div className="erl-modal-title">
<div>上传历史报告</div>
<div className="erl-modal-subtitle">
上传过去三年已完成并正式签发的安全评价报告
</div>
</div>
}
open={addVisible}
onCancel={() => setAddVisible(false)}
onOk={handleAddOk}
confirmLoading={evalReportAddLoading}
okText="确认上传"
cancelText="取消"
width={820}
destroyOnClose
className="erl-upload-modal"
>
<Form form={uploadForm} layout="vertical" preserve={false}>
<Form.Item name="fileUrl" hidden>
<Input />
</Form.Item>
<Form.Item name="fileName" hidden>
<Input />
</Form.Item>
<Form.Item
name="file"
rules={[
{
validator: async () => {
if (!uploadForm.getFieldValue("fileUrl")) {
throw new Error("请选择正式报告文件");
}
},
},
]}
>
<div className="erl-upload-box">
<Upload
name="file"
action={UPLOAD_ACTION}
headers={{ token: sessionStorage.getItem("token") || "" }}
onChange={handleFileUploadChange}
accept=".pdf,.doc,.docx"
maxCount={1}
showUploadList={false}
beforeUpload={(file) => {
const isLt100M = file.size / 1024 / 1024 <= 100;
if (!isLt100M) {
message.error("单个文件不超过 100MB");
return Upload.LIST_IGNORE;
}
return true;
}}
className="erl-upload-trigger"
>
<div className="erl-upload-box-inner">
<div className="erl-upload-icon">PDF</div>
<div className="erl-upload-text">
<strong>选择正式报告文件</strong>
<p>
支持 PDFWord建议上传签字盖章后的正式 PDF 文件单个文件不超过100MB
</p>
<span>
{uploadedFile
? `${uploadedFile.name}`
: "尚未选择文件"}
</span>
</div>
<Button type="primary" size="small" icon={<UploadOutlined />}>
选择文件
</Button>
</div>
</Upload>
</div>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="reportNo"
label="报告编号"
rules={[{ required: true, message: "请输入报告编号" }]}
>
<Input placeholder="例如CQAP-XZ-2026-001" maxLength={64} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="reportName"
label="报告名称"
rules={[{ required: true, message: "请输入报告名称" }]}
>
<Input placeholder="请输入正式报告名称" maxLength={200} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="reportYear" label="报告年度">
<Select options={getYearOptions()} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="evalTypeCode" label="评价类型">
<Select options={REPORT_EVAL_TYPE_OPTIONS} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="industryCode" label="所属行业">
<Select options={REPORT_INDUSTRY_OPTIONS} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="evaluatedUnitName"
label="被评价单位"
rules={[{ required: true, message: "请输入被评价单位" }]}
>
<Input placeholder="请输入被评价单位名称" maxLength={200} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="issueDate"
label="报告签发日期"
rules={[{ required: true, message: "请选择签发日期" }]}
>
<DatePicker style={{ width: "100%" }} placeholder="选择签发日期" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="pageCount" label="报告页数">
<InputNumber
style={{ width: "100%" }}
min={1}
placeholder="例如186"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="submitToRegulator" label="入库后处理">
<Select
options={[
{ label: "仅存入机构报告库", value: 0 },
{ label: "同步报送监管端抽查", value: 1 },
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="secretLevelCode" label="保密属性">
<Select options={SECRET_LEVEL_OPTIONS} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="合同上传">
<Upload
name="file"
action={UPLOAD_ACTION}
headers={{ token: sessionStorage.getItem("token") || "" }}
onChange={handleContractUploadChange}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.zip,.rar"
maxCount={20}
beforeUpload={(file) => {
const isLt100M = file.size / 1024 / 1024 <= 100;
if (!isLt100M) {
message.error("单个文件不超过 100MB");
return Upload.LIST_IGNORE;
}
return true;
}}
>
<Button icon={<UploadOutlined />}>选择合同文件</Button>
</Upload>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="其他过程文档">
<Upload
name="file"
action={UPLOAD_ACTION}
headers={{ token: sessionStorage.getItem("token") || "" }}
onChange={handleProcessDocUploadChange}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.zip,.rar"
maxCount={20}
beforeUpload={(file) => {
const isLt100M = file.size / 1024 / 1024 <= 100;
if (!isLt100M) {
message.error("单个文件不超过 100MB");
return Upload.LIST_IGNORE;
}
return true;
}}
>
<Button icon={<UploadOutlined />}>选择过程文档</Button>
</Upload>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="remarks" label="备注">
<TextArea
rows={3}
placeholder="填写报告修订版次、附件情况或脱密说明"
maxLength={500}
showCount
/>
</Form.Item>
</Col>
</Row>
</Form>
<div className="erl-upload-tip">
<strong>上传要求</strong>
<span>报告编号不得重复</span>
<span>报告须完成签字盖章</span>
<span>分类信息用于监管抽查检索</span>
<span>涉密内容须先完成脱密处理</span>
</div>
</Modal>
{/* 报告详情 */}
<Modal
title="报告详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={<Button onClick={() => setDetailVisible(false)}>关闭</Button>}
width={760}
destroyOnClose
>
{detailData ? (
<>
<Descriptions bordered column={{ xs: 1, sm: 1, md: 2 }} size="small">
<Descriptions.Item label="报告编号">
<span className="erl-report-no">{detailData.reportNo || "-"}</span>
</Descriptions.Item>
<Descriptions.Item label="报告名称">
{detailData.reportName || "-"}
</Descriptions.Item>
<Descriptions.Item label="报告年度">
{detailData.reportYear || "-"}
</Descriptions.Item>
<Descriptions.Item label="评价类型">
{detailData.evalTypeName ||
getOptionLabel(REPORT_EVAL_TYPE_OPTIONS, detailData.evalTypeCode)}
</Descriptions.Item>
<Descriptions.Item label="所属行业">
{detailData.industryName ||
getOptionLabel(REPORT_INDUSTRY_OPTIONS, detailData.industryCode)}
</Descriptions.Item>
<Descriptions.Item label="被评价单位">
{detailData.evaluatedUnitName || "-"}
</Descriptions.Item>
<Descriptions.Item label="签发日期">
{detailData.issueDate
? dayjs(detailData.issueDate).format("YYYY-MM-DD")
: "-"}
</Descriptions.Item>
<Descriptions.Item label="报告页数">
{detailData.pageCount ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="报送状态">
{renderDisplayStatus(detailData)}
</Descriptions.Item>
<Descriptions.Item label="报告状态">
{detailData.reportStatusName || "-"}
</Descriptions.Item>
<Descriptions.Item label="保密属性">
{detailData.secretLevelName ||
getOptionLabel(SECRET_LEVEL_OPTIONS, detailData.secretLevelCode)}
</Descriptions.Item>
<Descriptions.Item label="归档时间">
{detailData.archiveTime
? dayjs(detailData.archiveTime).format("YYYY-MM-DD HH:mm")
: "-"}
</Descriptions.Item>
<Descriptions.Item label="文件名称" span={2}>
<div className="erl-file-cell">
<i>{getFileExt(detailData.fileName || detailData.fileUrl)}</i>
<span>{detailData.fileName || "-"}</span>
</div>
</Descriptions.Item>
<Descriptions.Item label="合同文件" span={2}>
{renderFileLinks(detailData.contractFileUrls)}
</Descriptions.Item>
<Descriptions.Item label="过程文档" span={2}>
{renderFileLinks(detailData.processDocUrls)}
</Descriptions.Item>
<Descriptions.Item label="备注" span={2}>
{detailData.remarks || "-"}
</Descriptions.Item>
</Descriptions>
<div className="erl-detail-actions">
<Button
type="primary"
onClick={() => handleDownload(detailData)}
disabled={!detailData.fileUrl}
>
下载报告
</Button>
{isUnsubmitted(detailData) && (
<Button onClick={() => handleSubmitToRegulator(detailData)}>
报送监管
</Button>
)}
{isSpotcheckRelated(detailData) && (
<Button
onClick={() => {
setDetailVisible(false);
handleViewSpotcheck(detailData);
}}
>
抽查记录
</Button>
)}
</div>
</>
) : (
<div className="erl-loading">
{evalReportDetailLoading ? "加载中..." : "暂无数据"}
</div>
)}
</Modal>
{/* 抽查记录 */}
<Modal
title={
currentReport
? `抽查记录 — ${currentReport.reportName || currentReport.reportNo || ""}`
: "抽查记录"
}
open={spotcheckVisible}
onCancel={() => setSpotcheckVisible(false)}
footer={<Button onClick={() => setSpotcheckVisible(false)}>关闭</Button>}
width={980}
destroyOnClose
>
<Table
rowKey="id"
columns={spotcheckColumns}
dataSource={spotcheckData}
loading={evalReportSpotcheckListLoading}
pagination={false}
size="small"
scroll={{ x: 1100 }}
locale={{ emptyText: "暂无抽查记录" }}
/>
</Modal>
{/* 整改反馈 */}
<Modal
title="提交整改反馈"
open={rectifyVisible}
onCancel={() => setRectifyVisible(false)}
onOk={handleRectifyOk}
confirmLoading={evalReportSpotcheckRectifyLoading}
okText="提交整改"
cancelText="取消"
width={560}
destroyOnClose
>
{rectifyRecord && (
<div className="erl-rectify-meta">
<div>
<label>抽查意见</label>
<span>{rectifyRecord.checkOpinion || "-"}</span>
</div>
<div>
<label>整改要求</label>
<span>{rectifyRecord.rectifyRequire || "-"}</span>
</div>
</div>
)}
<Form form={rectifyForm} layout="vertical" preserve={false}>
<Form.Item
name="rectifyFeedback"
label="整改反馈"
rules={[{ required: true, message: "请填写整改反馈" }]}
>
<TextArea
rows={4}
placeholder="请详细说明整改措施和完成情况..."
showCount
maxLength={500}
/>
</Form.Item>
</Form>
</Modal>
</PageLayout>
</ConfigProvider>
);
};
export default Connect(
[NS_SAFETY_EVAL_BUSINESS],
true,
)(AntdTableFuncControl(EvalReportLibrary));