import React, { useEffect, useState } from "react";
import {
Form,
Table,
Button,
Modal,
Input,
Select,
ConfigProvider,
message,
Upload,
} from "antd";
import { UploadOutlined } 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 { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_INSP_NOTICE } from "~/enumerate/namespace";
import dayjs from "dayjs";
import "./index.less";
const { TextArea } = Input;
const API_HOST = window.process?.env?.app?.API_HOST || "";
const UPLOAD_ACTION = `${API_HOST}/safetyEval/file/upload`;
/** 雪花 ID 须保持字符串,避免 Number() 精度丢失 */
const toId = (id) => (id == null ? id : String(id));
const downloadBinaryPost = async (path, body, fallbackName) => {
const headers = {
"Content-Type": "application/json",
token: sessionStorage.getItem("token") || "",
};
const orgInfoId = sessionStorage.getItem("orgInfoId");
if (orgInfoId) {
headers.orgInfoId = String(orgInfoId);
}
const res = await fetch(`${API_HOST}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body || {}),
});
const contentType = res.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const json = await res.json();
throw new Error(json.errMessage || json.message || "导出失败");
}
if (!res.ok) {
throw new Error(`请求失败(${res.status})`);
}
const blob = await res.blob();
let fileName = fallbackName;
const disposition = res.headers.get("content-disposition") || "";
const matched = disposition.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
if (matched?.[1]) {
try {
fileName = decodeURIComponent(matched[1].replace(/"/g, ""));
} catch {
fileName = matched[1].replace(/"/g, "");
}
}
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = fileName;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
const PROTO_THEME = {
token: { borderRadius: 8, borderRadiusLG: 12, borderRadiusSM: 6 },
};
/** 通知类型与监管端监督检查告知一致 */
const INSP_TYPE_OPTIONS = [
{ label: "项目过程检查", value: "PROJECT_PROCESS" },
{ label: "资质保持检查", value: "QUAL_KEEP" },
{ label: "专项检查", value: "SPECIAL" },
];
const TAB_STATUS = {
all: undefined,
unread: 1,
processing: 2,
feedback: 3,
closed: 4,
};
const TAB_ITEMS = [
{ key: "all", label: "全部通知" },
{ key: "unread", label: "待处理" },
{ key: "processing", label: "处理中" },
{ key: "feedback", label: "待监管复核" },
{ key: "closed", label: "已办结" },
];
const STATUS_FILTER_OPTIONS = [
{ label: "待处理", value: 1 },
{ label: "处理中", value: 2 },
{ label: "待监管复核", value: 3 },
{ label: "已办结", value: 4 },
];
const PROCESS_TYPE_OPTIONS = [
{ label: "提交整改材料", value: "提交整改材料" },
{ label: "提交核查说明", value: "提交核查说明" },
{ label: "补充证明材料", value: "补充证明材料" },
{ label: "查看办结结果", value: "查看办结结果" },
];
const statusTagClass = (code) => {
if (code === 1) return "inst-tag-danger";
if (code === 2) return "inst-tag-warning";
if (code === 3) return "inst-tag-info";
if (code === 4) return "inst-tag-success";
return "";
};
const statusDisplayName = (code, name) => {
if (code === 1) return "待处理";
if (code === 2) return "处理中";
if (code === 3) return "待监管复核";
if (code === 4) return "已办结";
return name || "-";
};
/** 列表「通知类型」展示:办结用办结通知,其余用检查类型名 */
const noticeTypeLabel = (record) => {
if (record.statusCode === 4) return "办结通知";
return record.inspTypeName || "监督检查通知";
};
const noticeTypeTagClass = (record) => {
if (record.statusCode === 4) return "inst-tag-success";
if (record.statusCode === 1) return "inst-tag-danger";
if (record.statusCode === 3) return "inst-tag-info";
return "inst-tag-warning";
};
const formatDate = (val) => {
if (!val) return "-";
const d = dayjs(val);
return d.isValid() ? d.format("YYYY-MM-DD") : val;
};
const resolveFileUrl = (raw) => {
if (!raw) return "";
const u = String(raw);
if (/^https?:\/\//i.test(u)) return u;
const base = window.fileUrl || "";
return base ? `${base}${u}` : u;
};
const parseUrlList = (val) => {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
if (typeof val === "string") {
try {
const parsed = JSON.parse(val);
if (Array.isArray(parsed)) return parsed.filter(Boolean);
} catch {
/* ignore */
}
return val
.split(/[,;\n]/)
.map((s) => s.trim())
.filter(Boolean);
}
return [];
};
const urlToFileItem = (url) => ({
fileUrl: url,
fileName: String(url).split("?")[0].split("/").pop() || "附件",
});
const collectViewFiles = (detail, materials = []) => {
const fromMaterials = (materials || []).filter((m) => m.fileUrl);
const fromNotice = parseUrlList(detail?.noticeUrls).map(urlToFileItem);
const fromResult = parseUrlList(detail?.resultUrls).map(urlToFileItem);
const seen = new Set();
return [...fromMaterials, ...fromNotice, ...fromResult].filter((item) => {
const key = resolveFileUrl(item.fileUrl);
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
};
const materialFileName = (m) =>
m.fileName || m.materialName || m.name || String(m.fileUrl || "").split("?")[0].split("/").pop() || "附件";
const ModalSection = ({ title, desc, children }) => (
{title}
{desc ?
{desc}
: null}
{children}
);
const InstitutionInspectionNotice = (props) => {
const [searchForm] = Form.useForm();
const [processForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [pageIndex, setPageIndex] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [activeTab, setActiveTab] = useState("all");
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [metrics, setMetrics] = useState({
total: 0,
pending: 0,
feedback: 0,
closed: 0,
});
const [processOpen, setProcessOpen] = useState(false);
const [processViewOnly, setProcessViewOnly] = useState(false);
const [detail, setDetail] = useState(null);
const [materials, setMaterials] = useState([]);
const [uploadUrls, setUploadUrls] = useState([]);
const [exportOpen, setExportOpen] = useState(false);
const [exportLoading, setExportLoading] = useState(false);
const {
institutionInspNoticePageLoading,
institutionInspNoticeGetLoading,
institutionInspNoticeConfirmLoading,
institutionInspNoticeFeedbackLoading,
institutionInspNoticeMaterialUploadLoading,
} = props;
const buildQuery = (tab = activeTab) => {
const values = searchForm.getFieldsValue();
const tabStatus = TAB_STATUS[tab];
return {
keyword: values.keyword || undefined,
inspTypeCode: values.inspTypeCode || undefined,
statusCode: values.statusCode ?? tabStatus,
};
};
const fetchMetrics = async () => {
try {
const [allRes, pendingRes, feedbackRes, closedRes] = await Promise.all([
props.institutionInspNoticePage({ current: 1, size: 1 }),
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 1 }),
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 3 }),
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 4 }),
]);
setMetrics({
total: allRes?.total || 0,
pending: pendingRes?.total || 0,
feedback: feedbackRes?.total || 0,
closed: closedRes?.total || 0,
});
} catch {
/* ignore */
}
};
const fetchData = async (page = pageIndex, size = pageSize, tab = activeTab) => {
const res = await props.institutionInspNoticePage({
...buildQuery(tab),
current: page,
size,
});
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
setPageIndex(page);
setPageSize(size);
}
};
useEffect(() => {
fetchData(1, 10, "all");
fetchMetrics();
}, []);
const switchTab = (key) => {
setActiveTab(key);
searchForm.setFieldsValue({ statusCode: undefined });
fetchData(1, pageSize, key);
};
const openProcess = async (record, viewOnly = false) => {
setProcessViewOnly(viewOnly);
setProcessOpen(true);
setDetail(null);
setMaterials([]);
setUploadUrls([]);
processForm.resetFields();
processForm.setFieldsValue({
noticeNo: record.noticeNo,
processType: "提交整改材料",
processRemark: "请根据监管要求填写问题原因、处理措施和完成情况。",
});
const [detailRes, matRes] = await Promise.all([
props.institutionInspNoticeGet({ id: String(record.id) }),
props.institutionInspNoticeMaterials({ noticeId: toId(record.id) }),
]);
if (detailRes?.success !== false) {
const data = detailRes?.data || null;
setDetail(data);
processForm.setFieldsValue({ noticeNo: data?.noticeNo || record.noticeNo });
if (data?.statusCode === 4) {
processForm.setFieldsValue({ processType: "查看办结结果" });
}
} else {
}
if (matRes?.success !== false) {
setMaterials(matRes?.data || []);
}
};
const onFileUpload = (info) => {
if (info.file.status === "done") {
const responseData = info.file.response?.data || info.file.response || {};
const fileUrl = responseData?.url || responseData?.fileUrl || "";
if (!fileUrl) {
message.error("上传成功但未返回文件地址");
return;
}
setUploadUrls((prev) => [...prev, fileUrl]);
} else if (info.file.status === "error") {
message.error("上传失败");
}
};
const handleSubmit = async () => {
if (!detail?.id) return;
const values = await processForm.validateFields();
if (values.processType === "查看办结结果" || detail.statusCode === 4) {
setProcessOpen(false);
return;
}
let current = detail;
if (current.statusCode === 1) {
const confirmRes = await props.institutionInspNoticeConfirm({
id: toId(current.id),
});
if (confirmRes?.success === false) {
return;
}
current = confirmRes?.data || { ...current, statusCode: 2 };
}
const pendingMaterials = materials.filter(
(m) => m.uploadStatusCode !== 2 && !m.fileUrl,
);
const urls = [...uploadUrls];
for (let i = 0; i < Math.min(urls.length, pendingMaterials.length); i += 1) {
const mat = pendingMaterials[i];
const upRes = await props.institutionInspNoticeMaterialUpload({
materialId: toId(mat.id),
fileUrl: urls[i],
});
if (upRes?.success === false) {
return;
}
}
if (current.statusCode === 2 || detail.statusCode === 2 || detail.statusCode === 1) {
const matRes = await props.institutionInspNoticeMaterials({
noticeId: toId(detail.id),
});
const latest = matRes?.data || materials;
const feedbackRes = await props.institutionInspNoticeFeedback({
id: toId(detail.id),
materials: latest
.filter((m) => m.fileUrl || urls.length)
.map((m, idx) => ({
materialId: toId(m.id),
fileUrl: m.fileUrl || urls[idx] || urls[0],
}))
.filter((m) => m.fileUrl),
});
if (feedbackRes?.success === false) {
return;
}
}
message.success("处理材料已提交监管复核");
setProcessOpen(false);
fetchData(pageIndex, pageSize);
fetchMetrics();
};
const handleExport = async () => {
setExportLoading(true);
try {
const query = buildQuery();
const hasSelected = selectedRowKeys.length > 0;
const body = {
...query,
scope: hasSelected ? "SELECTED" : "FILTERED",
ids: hasSelected ? selectedRowKeys.map(toId) : undefined,
};
await downloadBinaryPost(
"/safetyEval/institution/insp-notice/export-messages",
body,
"监管消息清单.csv",
);
message.success("监管消息清单已导出");
setExportOpen(false);
} catch (e) {
message.error(e.message || "导出失败");
} finally {
setExportLoading(false);
}
};
const columns = [
{
title: "通知编号",
dataIndex: "noticeNo",
width: 140,
render: (t) => {t || "-"},
},
{
title: "通知类型",
dataIndex: "inspTypeName",
width: 110,
render: (_, record) => (
{noticeTypeLabel(record)}
),
},
{
title: "来源预警",
dataIndex: "sourceRefNo",
width: 140,
render: (t) => t || "-",
},
{
title: "来源字段",
key: "sourceFields",
width: 180,
render: (_, record) => (
{record.sourceTypeName ? (
{record.sourceTypeName}
) : (
监督检查
)}
{record.inspTypeName ? (
{record.inspTypeName}
) : null}
),
},
{
title: "监管要求",
dataIndex: "noticeContent",
ellipsis: true,
width: 240,
},
{
title: "办理期限",
dataIndex: "planInspTime",
width: 120,
render: formatDate,
},
{
title: "状态",
dataIndex: "statusCode",
width: 110,
render: (code, record) => (
{statusDisplayName(code, record.statusName)}
),
},
{
title: "操作",
key: "action",
width: 90,
align: "center",
fixed: "right",
render: (_, record) =>
record.statusCode === 1 ? (
) : (
),
},
];
const submitting =
institutionInspNoticeConfirmLoading ||
institutionInspNoticeFeedbackLoading ||
institutionInspNoticeMaterialUploadLoading;
return (
接收监管端下发的整改通知、监督检查通知、核查函及其他通知文件,机构在线反馈结果、上传证明材料并查看监管复核意见。
{TAB_ITEMS.map((tab) => (
))}
监管消息总数
{metrics.total}
来源:监管端风险预警中心
✉️
待处理
{metrics.pending}
需机构直接处理
待
待监管复核
{metrics.feedback}
材料已提交
审
已办结
{metrics.closed}
办结消息可归档
结
消息来源:
监管端风险预警中心。监管人员在风险预警中心发出整改通知、核查函、补正通知或办结结果后,机构端在此接收并处理;资质保持监控页面只作为监管信息来源。
,
,
,
]}
onFinish={() => fetchData(1, pageSize)}
onReset={() => {
searchForm.resetFields();
setActiveTab("all");
fetchData(1, pageSize, "all");
}}
/>
监管消息列表:展示来源预警、来源字段、办理期限和机构处理状态。
`共 ${t} 条`,
}}
onChange={(pag) => fetchData(pag.current, pag.pageSize)}
/>
{/* 处理 — 对齐原型 regMessageProcess */}
setProcessOpen(false)}
width={820}
destroyOnClose
className="inst-modal"
footer={
processViewOnly ? (
) : (
)
}
>
{institutionInspNoticeGetLoading || !detail ? (
加载中...
) : (
<>
>
)}
{/* 导出 — 对齐原型 regMessageExport */}
setExportOpen(false)}
width={560}
destroyOnClose
className="inst-modal"
footer={
}
>
);
};
export default Connect(
[NS_INSP_NOTICE],
true,
)(AntdTableFuncControl(InstitutionInspectionNotice));