dev-tmp1
tangjie 2026-08-18 12:18:14 +08:00
parent cc68fb3c9e
commit 17f193025b
3 changed files with 408 additions and 517 deletions

View File

@ -10,19 +10,17 @@ import {
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 TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
@ -41,7 +39,25 @@ const { TextArea } = Input;
const UPLOAD_ACTION = `${window.process?.env?.app?.API_HOST || ""}/safetyEval/file/upload`;
/** 报告文件类型配置 */
const FILE_TYPE_CONFIG = {
main: {
label: "报告正文",
accept: ".pdf,.doc,.docx,.zip,.rar,.7z",
acceptSet: new Set(["pdf", "doc", "docx", "zip", "rar", "7z"]),
rule: "正式签字盖章版正文",
format: "PDF、Word、ZIP、RAR、7Z单个文件不超过100MB",
},
attachment: {
label: "报告附件",
accept: ".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip,.rar,.7z",
acceptSet: new Set(["pdf", "doc", "docx", "xls", "xlsx", "jpg", "jpeg", "png", "zip", "rar", "7z"]),
rule: "图纸、检测报告、签字页及支撑资料",
format: "PDF、Office、图片、ZIP、RAR、7Z",
},
};
let fileInputCounter = 0;
/** 保密属性OpenAPI 仅示例 NORMAL */
const SECRET_LEVEL_OPTIONS = [
@ -85,30 +101,6 @@ const getYearOptions = () => {
});
};
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 || "-";
@ -131,19 +123,16 @@ const EvalReportLibrary = (props) => {
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 [reportFiles, setReportFiles] = useState([]);
const {
evalReportPageLoading,
@ -153,6 +142,9 @@ const EvalReportLibrary = (props) => {
evalReportSpotcheckRectifyLoading,
} = props.safetyEvalBusiness;
// 查看模式:复用上传弹窗展示详情,表单整体禁用
const viewMode = detailData !== null;
const fetchSummary = async () => {
const res = await props.evalReportSummary();
if (res?.success !== false) {
@ -200,122 +192,116 @@ const EvalReportLibrary = (props) => {
};
const handleOpenAdd = () => {
setUploadedFile(null);
setContractFiles([]);
setProcessDocFiles([]);
setDetailData(null);
uploadForm.resetFields();
uploadForm.setFieldsValue({
reportYear: dayjs().year(),
evalTypeCode: "PRE",
secretLevelCode: "NORMAL",
issueDate: dayjs(),
submitToRegulator: 0,
});
fileInputCounter = 0;
setReportFiles([
{ id: ++fileInputCounter, type: "main", url: "", fileName: "" },
{ id: ++fileInputCounter, type: "attachment", url: "", fileName: "" },
]);
setAddVisible(true);
};
const handleFileUploadChange = (info) => {
if (info.file.status === "uploading") return;
const addReportFileRow = () => {
setReportFiles((prev) => [
...prev,
{ id: ++fileInputCounter, type: "attachment", url: "", fileName: "" },
]);
message.success("已添加一条文件记录");
};
const removeReportFileRow = (id) => {
setReportFiles((prev) => {
if (prev.length <= 1) {
message.warning("至少保留一条文件记录");
return prev;
}
return prev.filter((f) => f.id !== id);
});
};
const changeReportFileType = (id, type) => {
setReportFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, type, url: "", fileName: "", uploading: false } : f)),
);
};
const handleReportFileUpload = (id, info) => {
if (info.file.status === "uploading") {
setReportFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, fileName: info.file.name, uploading: true } : f)),
);
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) {
const data = info.file.response?.data;
const url = data?.url || data?.fileUrl || "";
const name = data?.name || data?.fileName || info.file.name;
if (!url) {
message.error("上传响应中未获取到文件地址");
return;
}
setUploadedFile({ url: fileUrl, name: fileName });
uploadForm.setFieldsValue({
fileUrl,
fileName,
reportName:
uploadForm.getFieldValue("reportName") ||
fileName.replace(/\.[^.]+$/, ""),
});
message.success(`${fileName} 上传成功`);
const { createdTime, size } = data || {};
setReportFiles((prev) =>
prev.map((f) =>
f.id === id
? { ...f, url, fileName: name, createdTime, size, uploading: false }
: f,
),
);
message.success(`${name} 上传成功`);
} else if (info.file.status === "error") {
setReportFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, fileName: "", uploading: false } : f)),
);
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("请选择需要上传的正式报告文件");
const uploading = reportFiles.some((r) => r.uploading);
if (uploading) {
message.warning("文件正在上传中,请稍候");
return;
}
const doneFiles = reportFiles.filter((r) => r.url);
const mainFiles = doneFiles.filter((r) => r.type === "main");
if (mainFiles.length === 0) {
message.warning("请至少上传1个报告正文");
return;
}
const fileUrlArr = doneFiles.map((r) => ({
type: r.type,
url: r.url,
name: r.fileName,
createdTime: r.createdTime,
size: r.size,
}));
const payload = {
...values,
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,
),
evalTypeName: getOptionLabel(EVAL_TYPE_OPTIONS, values.evalTypeCode),
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,
fileUrl: JSON.stringify(fileUrlArr),
secretLevelName: getOptionLabel(SECRET_LEVEL_OPTIONS, values.secretLevelCode),
submitToRegulator: values.submitToRegulator === 1,
};
delete payload.reportMainFile;
delete payload.reportAttachment;
const res = await props.evalReportAdd(payload);
if (res?.success !== false) {
message.success(
@ -330,23 +316,76 @@ const EvalReportLibrary = (props) => {
};
const handleViewDetail = async (record) => {
setDetailData(null);
setDetailVisible(true);
// 查看复用上传弹窗:清空表单并置为只读,接口返回后回填数据
setDetailData({});
uploadForm.resetFields();
fileInputCounter = 0;
setReportFiles([]);
setAddVisible(true);
const res = await props.evalReportDetail({ id: record.id });
if (res?.success !== false) {
setDetailData(res?.data || {});
if (res?.success === false) {
setAddVisible(false);
return;
}
const data = res?.data || {};
setDetailData(data);
uploadForm.setFieldsValue({
...data,
issueDate: data.issueDate ? dayjs(data.issueDate) : undefined,
});
let files = [];
try {
const parsed = JSON.parse(data.fileUrl);
if (Array.isArray(parsed)) files = parsed;
} catch {
if (data.fileUrl) {
files = [{ type: "main", url: data.fileUrl, name: data.fileName || data.reportName }];
}
}
setReportFiles(
files.map((f) => ({
id: ++fileInputCounter,
type: f.type,
url: f.url,
fileName: f.name,
})),
);
};
const handleDownload = async (record) => {
let fileUrl = record.fileUrl;
let fileName = record.fileName || record.reportName || "报告文件";
// 解析 fileUrl兼容新格式JSON 数组)和旧格式(字符串)
if (fileUrl) {
try {
const arr = JSON.parse(fileUrl);
if (Array.isArray(arr) && arr.length > 0) {
const mainFile = arr.find((f) => f.type === "main") || arr[0];
fileUrl = mainFile.url;
fileName = mainFile.name || fileName;
}
} catch {
// 旧格式:直接使用字符串
}
}
if (!fileUrl) {
const res = await props.evalReportDetail({ id: record.id });
if (res?.success === false) return;
fileUrl = res?.data?.fileUrl;
const detailFileUrl = res?.data?.fileUrl;
if (detailFileUrl) {
try {
const arr = JSON.parse(detailFileUrl);
if (Array.isArray(arr) && arr.length > 0) {
const mainFile = arr.find((f) => f.type === "main") || arr[0];
fileUrl = mainFile.url;
fileName = mainFile.name || fileName;
}
} catch {
fileUrl = detailFileUrl;
fileName = res?.data?.fileName || fileName;
}
}
}
if (!fileUrl) {
message.warning("文件地址不存在");
return;
@ -864,15 +903,19 @@ const EvalReportLibrary = (props) => {
/>
</div>
{/* 上传历史报告 */}
{/* 上传历史报告 / 报告详情查看复用同一弹窗disabled 只读) */}
<Modal
title={
viewMode ? (
"报告详情"
) : (
<div className="erl-modal-title">
<div>上传历史报告</div>
<div className="erl-modal-subtitle">
上传过去三年已完成并正式签发的安全评价报告
</div>
</div>
)
}
open={addVisible}
onCancel={() => setAddVisible(false)}
@ -880,6 +923,23 @@ const EvalReportLibrary = (props) => {
confirmLoading={evalReportAddLoading}
okText="确认上传"
cancelText="取消"
footer={
viewMode ? (
<Space>
{detailData && isSpotcheckRelated(detailData) && (
<Button
onClick={() => {
setAddVisible(false);
handleViewSpotcheck(detailData);
}}
>
抽查记录
</Button>
)}
<Button onClick={() => setAddVisible(false)}>关闭</Button>
</Space>
) : undefined
}
width={820}
destroyOnHidden
className="erl-upload-modal"
@ -889,69 +949,8 @@ const EvalReportLibrary = (props) => {
layout="vertical"
preserve={false}
scrollToFirstError
disabled={viewMode}
>
<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
@ -1031,49 +1030,114 @@ const EvalReportLibrary = (props) => {
<Select options={SECRET_LEVEL_OPTIONS} />
</Form.Item>
</Col>
{/* 报告文件列表(对齐原型 .report-upload-files Table 布局) */}
<Col span={24}>
<Form.Item label="合同上传">
<div className="erl-report-files">
<div className="erl-report-files-head">
<h4>报告文件</h4>
{!viewMode && (
<Button type="link" size="small" icon={<PlusOutlined />} onClick={addReportFileRow}>
添加文件
</Button>
)}
</div>
<Table
dataSource={reportFiles}
rowKey="id"
pagination={false}
size="small"
loading={viewMode && evalReportDetailLoading}
columns={[
{
title: "文件类型",
dataIndex: "type",
width: 120,
render: (_, row) => (
<Select
value={row.type}
onChange={(val) => changeReportFileType(row.id, val)}
size="small"
style={{ width: "100%" }}
options={[
{ label: "报告正文", value: "main" },
{ label: "报告附件", value: "attachment" },
]}
/>
),
},
{
title: "上传要求",
dataIndex: "type",
render: (_, row) => {
const config = FILE_TYPE_CONFIG[row.type];
return (
<div style={{ fontSize: 13, color: "#334155" }}>
{row.type === "main" && <span style={{ color: "#dc2626" }}>* </span>}
{config.rule}
<div style={{ fontSize: 12, color: "#64748b", marginTop: 2 }}>
{config.format}
</div>
</div>
);
},
},
{
title: "文件",
dataIndex: "fileName",
render: (_, row) => (
<PreviewUrlButton url={row.url || ""}>
{row.uploading
? `正在上传 ${row.fileName}...`
: row.fileName || "尚未选择文件"}
</PreviewUrlButton>
),
},
!viewMode && {
title: "操作",
fixed: "right",
width: 140,
render: (_, row) => {
const config = FILE_TYPE_CONFIG[row.type];
return (
<TableAction>
<Upload
name="file"
action={UPLOAD_ACTION}
headers={{ token: sessionStorage.getItem("token") || "" }}
onChange={handleContractUploadChange}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.zip,.rar"
maxCount={20}
accept={config.accept}
showUploadList={false}
beforeUpload={(file) => {
const isLt100M = file.size / 1024 / 1024 <= 100;
if (!isLt100M) {
if (file.size / 1024 / 1024 > 100) {
message.error("单个文件不超过 100MB");
return Upload.LIST_IGNORE;
}
return true;
}}
onChange={(info) => handleReportFileUpload(row.id, info)}
>
<Button icon={<UploadOutlined />}>选择合同文件</Button>
<Button type="link" size="small" icon={<UploadOutlined />} loading={row.uploading}>
选择文件
</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
type="link"
size="small"
danger
onClick={() => removeReportFileRow(row.id)}
>
<Button icon={<UploadOutlined />}>选择过程文档</Button>
</Upload>
</Form.Item>
删除
</Button>
</TableAction>
);
},
},
].filter(Boolean)}
/>
</div>
<div className="erl-report-files-note">
<strong>附件材料提示</strong>
营业执照副本复印件不动产权证书复印件企业投资项目备案信息复印件专家评审意见修改说明附图地理位置示意图周边关系及总平面布置图
</div>
</Col>
<Col span={24}>
<Form.Item name="remarks" label="备注">
@ -1088,193 +1152,13 @@ const EvalReportLibrary = (props) => {
</Row>
</Form>
{!viewMode && (
<div className="erl-upload-tip">
<strong>上传要求</strong>
<span>报告编号不得重复</span>
<span>报告须完成签字盖章</span>
<span>分类信息用于监管抽查检索</span>
</div>
</Modal>
<Modal
title={
'报告详情'
}
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={<Button onClick={() => setDetailVisible(false)}>关闭</Button>}
width={820}
destroyOnHidden
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>

View File

@ -46,11 +46,6 @@
min-width: 0;
}
.erl-report-no {
font-size: 14px;
}
.erl-status-link {
display: inline-block;
}
@ -225,70 +220,42 @@
}
.erl-detail-actions {
/* 报告文件列表(对齐原型 .report-upload-files */
.erl-report-files {
border: 1px solid var(--micro-temp-color-border, #e8edf3);
border-radius: 6px;
overflow: hidden;
.erl-report-files-head {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 16px;
}
/* 变更时间2026-08-11 查看详情改为与「上传历史报告」一致的排版样式 */
.erl-detail-form {
margin-top: 16px;
}
.erl-detail-item {
display: flex;
align-items: baseline;
padding: 8px 10px;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #eef2f7;
border-radius: 8px;
min-height: 40px;
border-bottom: 1px solid #e8edf3;
label {
flex: 0 0 84px;
color: #64748b;
h4 {
margin: 0;
font-size: 14px;
color: #1e293b;
}
span {
flex: 1;
color: #1f2937;
font-size: 14px;
word-break: break-all;
}
}
/* 只读查看态的正式报告文件框:弱化边框,去掉上传交互 */
.erl-upload-box-view {
border-style: solid;
border-color: #e2e8f0;
background: #f8fafc;
}
.erl-report-files-note {
margin-top: 10px;
padding: 10px 14px;
border-left: 3px solid #2563eb;
border-radius: 4px;
background: #eff6ff;
color: #475569;
font-size: 13px;
line-height: 1.65;
.erl-detail-attachments {
margin-top: 16px;
.erl-detail-attachments-block {
margin-bottom: 12px;
strong {
color: #1e40af;
margin-right: 4px;
}
.erl-detail-attachments-title {
font-size: 14px;
color: #64748b;
margin-bottom: 6px;
}
.ant-btn-link {
padding-left: 0;
}
}
.erl-loading {
text-align: center;
padding: 40px 0;
color: #999;
}
.erl-rectify-meta {

View File

@ -225,6 +225,15 @@ const getFileExtLabel = (name = "") => {
return ext && ext !== name ? ext.toUpperCase() : "PDF";
};
/** 字节数转可读文件大小 */
const formatFileSize = (bytes) => {
const num = Number(bytes);
if (!num || Number.isNaN(num)) return "-";
if (num < 1024) return `${num} B`;
if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KB`;
return `${(num / 1024 / 1024).toFixed(1)} MB`;
};
/** 解析存储的 JSON 文件列表字符串,返回数组 */
const parseFileList = (jsonStr) => {
if (!jsonStr) return [];
@ -506,12 +515,37 @@ const EvalReportDatabase = (props) => {
const handleDownloadOne = async (record) => {
let fileUrl = record.fileUrl;
let fileName = record.fileName || record.reportName || "报告文件";
// 解析 fileUrl兼容新格式JSON 数组)和旧格式(字符串)
if (fileUrl) {
try {
const arr = JSON.parse(fileUrl);
if (Array.isArray(arr) && arr.length > 0) {
const mainFile = arr.find((f) => f.type === "main") || arr[0];
fileUrl = mainFile.url;
fileName = mainFile.name || fileName;
}
} catch {
// 旧格式:直接使用字符串
}
}
if (!fileUrl) {
const res = await props.regulatorEvalReportDetail({ id: record.id });
if (res?.success === false) return;
fileUrl = res?.data?.fileUrl;
const detailFileUrl = res?.data?.fileUrl;
if (detailFileUrl) {
try {
const arr = JSON.parse(detailFileUrl);
if (Array.isArray(arr) && arr.length > 0) {
const mainFile = arr.find((f) => f.type === "main") || arr[0];
fileUrl = mainFile.url;
fileName = mainFile.name || fileName;
}
} catch {
fileUrl = detailFileUrl;
fileName = res?.data?.fileName || fileName;
}
}
}
if (!fileUrl) {
message.warning("文件地址不存在");
return;
@ -1012,50 +1046,56 @@ const EvalReportDatabase = (props) => {
]}
/>
</ModalSection>
{(() => {
let files = [];
try {
const parsed = JSON.parse(detailData.fileUrl);
if (Array.isArray(parsed)) files = parsed;
} catch {
if (detailData.fileUrl) {
files = [{ type: "main", url: detailData.fileUrl, name: detailData.fileName || detailData.reportName }];
}
}
const mainFiles = files.filter((f) => f.type === "main");
const attachmentFiles = files.filter((f) => f.type !== "main");
const renderFileRows = (list) =>
list.map((f) => [
<PreviewUrlButton key={f.url} url={f.url}>
{f.name || "未命名文件"}
</PreviewUrlButton>,
getFileExtLabel(f.name || f.url),
formatFileSize(f.size),
f.createdTime
? dayjs(f.createdTime).format("YYYY-MM-DD HH:mm")
: "-",
<PreviewUrlButton key={`${f.url}-view`} url={f.url}>
查看
</PreviewUrlButton>,
]);
return (
<>
<ModalSection
title="报告文件"
desc="支持在线查看和下载正式报告。"
title={`报告正文(${mainFiles.length}份)`}
desc="评价机构上传的正式签字盖章版报告正文。"
>
<ProtoTable
headers={["文件名称", "文件类型", "版本"]}
rows={[
[
<PreviewUrlButton
url={detailData.fileUrl}
headers={["文件名称", "格式", "大小", "上传时间", "操作"]}
rows={mainFiles.length ? renderFileRows(mainFiles) : [["-", "-", "-", "-", "-"]]}
/>
</ModalSection>
<ModalSection
title={`报告附件(${attachmentFiles.length}份)`}
desc="附件按材料名称逐项展示,可在线查看或单独下载。"
>
{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),
])}
headers={["文件名称", "格式", "大小", "上传时间", "操作"]}
rows={attachmentFiles.length ? renderFileRows(attachmentFiles) : [["-", "-", "-", "-", "-"]]}
/>
</ModalSection>
</>
);
})()}
</>
) : (
<div className="redb-loading">
{regulatorEvalReportDetailLoading ? "加载中..." : "暂无数据"}