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

1379 lines
45 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 "~/components/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import PreviewUrlButton from "~/components/PreviewUrlButton";
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 {EVAL_TYPE_OPTIONS} from '~/enumerate/constant';
import {QUALIFICATION_INDUSTRY_OPTIONS} from '~/enumerate/enterpriseOptions';
import "./index.less";
/**
* 变更时间2026-08-11
* 变更原因:需求「机构端报告库批量报送 + 查看详情样式优化」:
* 1) 列表支持勾选「未报送」报告并一次性批量报送(调用 evalReportBatchSubmit接口未就绪时降级逐条 submit
* 2) 查看详情改为与「上传历史报告」表单一致的排版(更贴近新增报告的录入样式),附件支持图片放大预览、
* 其他文件点击新窗口预览/下载。
* 相关约束:后端代码不修改,仅作为接口/字段对照依据;批量报送接口需后端按约定契约新增。
*/
const { router } = tools;
const { TextArea } = Input;
const UPLOAD_ACTION = `${window.process?.env?.app?.API_HOST || ""}/safetyEval/file/upload`;
/** 保密属性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 文件列表,使用 PreviewUrlButton 渲染预览链接 */
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} style={{ marginBottom: 4 }}>
<PreviewUrlButton url={f.url} >
<a>{f.name || "未命名文件"}</a>
</PreviewUrlButton>
</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([]);
// 变更时间2026-08-11 变更原因:批量报送需记录勾选的报告主键
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [batchSubmitting, setBatchSubmitting] = useState(false);
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",
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(
EVAL_TYPE_OPTIONS,
values.evalTypeCode,
),
industryCode: values.industryCode,
industryName: getOptionLabel(
QUALIFICATION_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();
}
},
});
};
/**
* 批量报送变更时间2026-08-11
* 变更原因:机构端报告库需支持一次性把多份「未报送」报告报送至监管端,原仅支持单条报送。
* 实现策略:
* 1) 调用批量报送接口 evalReportBatchSubmitPOST /safetyEval/institution/eval-report/submit/batch入参 { ids: Long[] }
* 后端已实现:逐条复用单条报送规则,并返回 successCount/failCount/failList 明细;
* 2) 若批量接口异常(如未部署/网络错误),降级为并发调用单条 submit保证功能可用
* 并对失败项汇总提示,避免个别失败导致整体中断。
*/
const handleBatchSubmit = async () => {
const ids = selectedRowKeys;
if (!ids || ids.length === 0) {
message.warning("请先勾选需要报送的报告");
return;
}
Modal.confirm({
title: "确认批量报送",
icon: <SendOutlined style={{ color: "#1677ff" }} />,
content: (
<div
style={{
fontSize: 14,
color: "#333",
lineHeight: 1.8,
padding: "8px 0",
}}
>
确定要将已勾选的 <strong>{ids.length}</strong>{" "}
未报送报告报送至监管端吗报送后监管端可进行抽查审核
</div>
),
okText: "确认批量报送",
cancelText: "取消",
onOk: async () => {
setBatchSubmitting(true);
try {
const res = await props.evalReportBatchSubmit({ ids });
if (res?.success !== false && res?.data) {
// 后端已返回成功/失败明细,精确展示
const { successCount = 0, failCount = 0, failList = [] } = res.data;
if (failCount === 0) {
message.success(`已批量报送 ${successCount} 份报告至监管端`);
} else {
const reasons = failList
.map((f) => `报告ID ${f.id}${f.reason}`)
.join("");
message.warning(
`批量报送完成:${successCount} 份成功,${failCount} 份失败(${reasons}`,
);
}
} else if (res?.success !== false) {
message.success(`已批量报送 ${ids.length} 份报告至监管端`);
} else {
throw new Error(res?.message || "批量报送失败");
}
} catch (err) {
// 批量接口未就绪(如 404/网络异常)时降级为逐条报送,保证功能可用
message.info("批量接口暂未就绪,正在逐条报送...");
let successCount = 0;
const failNames = [];
await Promise.all(
ids.map(async (id) => {
try {
const r = await props.evalReportSubmit({ id });
if (r?.success !== false) successCount += 1;
else failNames.push(id);
} catch {
failNames.push(id);
}
}),
);
if (failNames.length === 0) {
message.success(`已逐条报送 ${successCount} 份报告至监管端`);
} else {
message.warning(
`逐条报送完成:${successCount} 份成功,${failNames.length} 份失败(请重试或检查报送状态)`,
);
}
} finally {
setBatchSubmitting(false);
setSelectedRowKeys([]);
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,
},
{
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(EVAL_TYPE_OPTIONS, record.evalTypeCode),
},
{
title: "所属行业",
dataIndex: "industryName",
width: 120,
render: (val, record) =>
val || getOptionLabel(QUALIFICATION_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">
<a onClick={() => handleViewDetail(record)}>查看</a>
<a onClick={() => handleDownload(record)}>下载</a>
{isUnsubmitted(record) && (
<a onClick={() => handleSubmitToRegulator(record)}>报送</a>
)}
</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={
/**
* 变更时间2026-08-11
* 变更原因:新增「批量报送」入口,与「上传历史报告」并列;仅在已勾选未报送报告时可点击,
* 按钮上直接反馈已选数量,降低误操作。
*/
<Space>
<Button
type="primary"
ghost
icon={<SendOutlined />}
disabled={selectedRowKeys.length === 0}
loading={batchSubmitting}
onClick={handleBatchSubmit}
>
批量报送
{selectedRowKeys.length > 0
? `${selectedRowKeys.length}`
: ""}
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleOpenAdd}
>
上传历史报告
</Button>
</Space>
}
>
<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={EVAL_TYPE_OPTIONS}
/>
</Form.Item>,
<Form.Item key="industryCode" name="industryCode">
<ControlWrapper.Select
label="所属行业"
placeholder="全部"
allowClear
options={QUALIFICATION_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}
/**
* 变更时间2026-08-11
* 变更原因:支持批量报送,需勾选功能;仅「未报送」记录可勾选,
* 已报送/抽查相关记录禁用勾选,避免重复报送。
*/
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
getCheckboxProps: (record) => ({
disabled: !isUnsubmitted(record),
}),
}}
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}
scrollToFirstError
>
<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={EVAL_TYPE_OPTIONS} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="industryCode" label="所属行业">
<Select options={QUALIFICATION_INDUSTRY_OPTIONS} placeholder="请选择所属行业" />
</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>
</div>
</Modal>
{/* 报告详情变更时间2026-08-11改为与「上传历史报告」一致的排版样式附件支持预览
* 变更原因:原型要求查看沿用新增报告的录入排版,字段顺序、分组与上传弹窗对齐,降低阅读认知成本;
* 附件(主报告/合同/过程文档)使用 PreviewUrlButton —— 图片点击放大预览,其他文件点击新窗口预览/下载。
*/}
<Modal
title={
<div className="erl-modal-title">
<div>报告详情</div>
<div className="erl-modal-subtitle">
报告库档案信息与上传录入样式一致
</div>
</div>
}
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={<Button onClick={() => setDetailVisible(false)}>关闭</Button>}
width={820}
destroyOnClose
className="erl-upload-modal"
>
{detailData ? (
<>
{/* 正式报告文件:预览优先,与上传弹窗主文件展示一致 */}
<div className="erl-upload-box erl-upload-box-view">
<div className="erl-upload-box-inner">
<div className="erl-upload-icon">
{getFileExt(detailData.fileName || detailData.fileUrl)}
</div>
<div className="erl-upload-text">
<strong>{detailData.reportName || "未命名报告"}</strong>
<p>
{detailData.fileName ||
detailData.fileUrl ||
"暂无正式报告文件"}
</p>
{detailData.fileUrl ? (
<PreviewUrlButton
url={detailData.fileUrl}
children={detailData.fileName || "预览报告"}
/>
) : (
<span>尚未上传正式报告文件</span>
)}
</div>
</div>
</div>
<Row gutter={16} className="erl-detail-form">
<Col span={12}>
<div className="erl-detail-item">
<label>报告编号</label>
<span className="erl-report-no">
{detailData.reportNo || "-"}
</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报告名称</label>
<span>{detailData.reportName || "-"}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报告年度</label>
<span>{detailData.reportYear || "-"}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>评价类型</label>
<span>
{detailData.evalTypeName ||
getOptionLabel(
EVAL_TYPE_OPTIONS,
detailData.evalTypeCode,
)}
</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>所属行业</label>
<span>
{detailData.industryName ||
getOptionLabel(
QUALIFICATION_INDUSTRY_OPTIONS,
detailData.industryCode,
)}
</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>被评价单位</label>
<span>{detailData.evaluatedUnitName || "-"}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报告签发日期</label>
<span>
{detailData.issueDate
? dayjs(detailData.issueDate).format("YYYY-MM-DD")
: "-"}
</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报告页数</label>
<span>{detailData.pageCount ?? "-"}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报送状态</label>
<span>{renderDisplayStatus(detailData)}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>报告状态</label>
<span>{detailData.reportStatusName || "-"}</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>保密属性</label>
<span>
{detailData.secretLevelName ||
getOptionLabel(
SECRET_LEVEL_OPTIONS,
detailData.secretLevelCode,
)}
</span>
</div>
</Col>
<Col span={12}>
<div className="erl-detail-item">
<label>归档时间</label>
<span>
{detailData.archiveTime
? dayjs(detailData.archiveTime).format(
"YYYY-MM-DD HH:mm",
)
: "-"}
</span>
</div>
</Col>
<Col span={24}>
<div className="erl-detail-item">
<label>合同文件</label>
<span>{renderFileLinks(detailData.contractFileUrls)}</span>
</div>
</Col>
<Col span={24}>
<div className="erl-detail-item">
<label>其他过程文档</label>
<span>{renderFileLinks(detailData.processDocUrls)}</span>
</div>
</Col>
<Col span={24}>
<div className="erl-detail-item">
<label>备注</label>
<span>{detailData.remarks || "-"}</span>
</div>
</Col>
</Row>
<div className="erl-detail-actions">
{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}
scrollToFirstError
>
<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));