2026-07-28 16:07:00 +08:00
|
|
|
|
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";
|
2026-08-13 10:26:12 +08:00
|
|
|
|
import SearchForm from "~/components/SearchForm";
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-17 14:17:45 +08:00
|
|
|
|
/** 通知类型与监管端监督检查告知一致 */
|
|
|
|
|
|
const INSP_TYPE_OPTIONS = [
|
|
|
|
|
|
{ label: "项目过程检查", value: "PROJECT_PROCESS" },
|
|
|
|
|
|
{ label: "资质保持检查", value: "QUAL_KEEP" },
|
|
|
|
|
|
{ label: "专项检查", value: "SPECIAL" },
|
2026-07-28 16:07:00 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-17 14:17:45 +08:00
|
|
|
|
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() || "附件";
|
|
|
|
|
|
|
2026-07-28 16:07:00 +08:00
|
|
|
|
const ModalSection = ({ title, desc, children }) => (
|
|
|
|
|
|
<div className="inst-modal-section">
|
|
|
|
|
|
<h4>{title}</h4>
|
|
|
|
|
|
{desc ? <p>{desc}</p> : null}
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-08-17 14:17:45 +08:00
|
|
|
|
const [processViewOnly, setProcessViewOnly] = useState(false);
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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 {
|
2026-08-17 14:17:45 +08:00
|
|
|
|
keyword: values.keyword || undefined,
|
|
|
|
|
|
inspTypeCode: values.inspTypeCode || undefined,
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-17 14:17:45 +08:00
|
|
|
|
const openProcess = async (record, viewOnly = false) => {
|
|
|
|
|
|
setProcessViewOnly(viewOnly);
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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 {
|
2026-08-14 19:27:34 +08:00
|
|
|
|
|
2026-07-28 16:07:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2026-08-14 19:27:34 +08:00
|
|
|
|
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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) {
|
2026-08-14 19:27:34 +08:00
|
|
|
|
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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) {
|
2026-08-14 19:27:34 +08:00
|
|
|
|
|
2026-07-28 16:07:00 +08:00
|
|
|
|
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) => <span className="inst-mono">{t || "-"}</span>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "通知类型",
|
|
|
|
|
|
dataIndex: "inspTypeName",
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<span className={`inst-tag ${noticeTypeTagClass(record)}`}>
|
|
|
|
|
|
{noticeTypeLabel(record)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "来源预警",
|
|
|
|
|
|
dataIndex: "sourceRefNo",
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (t) => t || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "来源字段",
|
|
|
|
|
|
key: "sourceFields",
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div className="inst-source-list">
|
|
|
|
|
|
{record.sourceTypeName ? (
|
|
|
|
|
|
<span className="inst-source-pill">{record.sourceTypeName}</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="inst-source-pill">监督检查</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{record.inspTypeName ? (
|
|
|
|
|
|
<span className="inst-source-pill">{record.inspTypeName}</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "监管要求",
|
|
|
|
|
|
dataIndex: "noticeContent",
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
width: 240,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "办理期限",
|
|
|
|
|
|
dataIndex: "planInspTime",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: formatDate,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "状态",
|
|
|
|
|
|
dataIndex: "statusCode",
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
render: (code, record) => (
|
|
|
|
|
|
<span className={`inst-tag ${statusTagClass(code)}`}>
|
|
|
|
|
|
{statusDisplayName(code, record.statusName)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
key: "action",
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
fixed: "right",
|
2026-08-17 14:17:45 +08:00
|
|
|
|
render: (_, record) =>
|
|
|
|
|
|
record.statusCode === 1 ? (
|
|
|
|
|
|
<Button type="primary" size="small" onClick={() => openProcess(record)}>
|
|
|
|
|
|
处理
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button type="primary" size="small" onClick={() => openProcess(record, true)}>
|
|
|
|
|
|
查看
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
2026-07-28 16:07:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const submitting =
|
|
|
|
|
|
institutionInspNoticeConfirmLoading ||
|
|
|
|
|
|
institutionInspNoticeFeedbackLoading ||
|
|
|
|
|
|
institutionInspNoticeMaterialUploadLoading;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ConfigProvider theme={PROTO_THEME}>
|
2026-08-03 17:16:23 +08:00
|
|
|
|
<PageLayout title="监管通知管理">
|
2026-07-28 16:07:00 +08:00
|
|
|
|
<div className="inst-page">
|
|
|
|
|
|
<div className="inst-page-header">
|
|
|
|
|
|
<div className="inst-sub">
|
|
|
|
|
|
接收监管端下发的整改通知、监督检查通知、核查函及其他通知文件,机构在线反馈结果、上传证明材料并查看监管复核意见。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="inst-tabs">
|
|
|
|
|
|
{TAB_ITEMS.map((tab) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={tab.key}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={`inst-tab${activeTab === tab.key ? " active" : ""}`}
|
|
|
|
|
|
onClick={() => switchTab(tab.key)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{tab.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="inst-metrics">
|
|
|
|
|
|
<div className="inst-metric-card">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="label">监管消息总数</div>
|
|
|
|
|
|
<div className="value">{metrics.total}</div>
|
|
|
|
|
|
<div className="hint">来源:监管端风险预警中心</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="icon">✉️</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="inst-metric-card">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="label">待处理</div>
|
|
|
|
|
|
<div className="value danger">{metrics.pending}</div>
|
|
|
|
|
|
<div className="hint">需机构直接处理</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="icon">待</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="inst-metric-card">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="label">待监管复核</div>
|
|
|
|
|
|
<div className="value warning">{metrics.feedback}</div>
|
|
|
|
|
|
<div className="hint">材料已提交</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="icon">审</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="inst-metric-card">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="label">已办结</div>
|
|
|
|
|
|
<div className="value success">{metrics.closed}</div>
|
|
|
|
|
|
<div className="hint">办结消息可归档</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="icon">结</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="inst-note">
|
|
|
|
|
|
<strong>消息来源:</strong>
|
|
|
|
|
|
监管端风险预警中心。监管人员在风险预警中心发出整改通知、核查函、补正通知或办结结果后,机构端在此接收并处理;资质保持监控页面只作为监管信息来源。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<SearchForm
|
|
|
|
|
|
form={searchForm}
|
|
|
|
|
|
loading={institutionInspNoticePageLoading}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
formLine={[
|
|
|
|
|
|
<Form.Item key="keyword" name="keyword">
|
|
|
|
|
|
<ControlWrapper.Input
|
|
|
|
|
|
label="通知关键词"
|
|
|
|
|
|
placeholder="通知编号/预警编号/事项"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
2026-08-17 14:17:45 +08:00
|
|
|
|
<Form.Item key="inspTypeCode" name="inspTypeCode">
|
2026-07-28 16:07:00 +08:00
|
|
|
|
<ControlWrapper.Select
|
|
|
|
|
|
label="通知类型"
|
|
|
|
|
|
placeholder="全部"
|
|
|
|
|
|
allowClear
|
2026-08-17 14:17:45 +08:00
|
|
|
|
options={INSP_TYPE_OPTIONS}
|
2026-07-28 16:07:00 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
<Form.Item key="statusCode" name="statusCode">
|
|
|
|
|
|
<ControlWrapper.Select
|
|
|
|
|
|
label="处理状态"
|
|
|
|
|
|
placeholder="全部"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
options={STATUS_FILTER_OPTIONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
]}
|
|
|
|
|
|
onFinish={() => fetchData(1, pageSize)}
|
|
|
|
|
|
onReset={() => {
|
|
|
|
|
|
searchForm.resetFields();
|
|
|
|
|
|
setActiveTab("all");
|
|
|
|
|
|
fetchData(1, pageSize, "all");
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="inst-toolbar">
|
|
|
|
|
|
<div className="inst-toolbar-left">
|
|
|
|
|
|
<span className="inst-hint">
|
|
|
|
|
|
监管消息列表:展示来源预警、来源字段、办理期限和机构处理状态。
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="inst-toolbar-right">
|
|
|
|
|
|
<Button size="small" onClick={() => setExportOpen(true)}>
|
|
|
|
|
|
导出消息
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
|
loading={institutionInspNoticePageLoading}
|
|
|
|
|
|
scroll={{ y: props.scrollY, x: 1200 }}
|
|
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys,
|
|
|
|
|
|
onChange: setSelectedRowKeys,
|
|
|
|
|
|
columnWidth: 34,
|
|
|
|
|
|
}}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
total,
|
|
|
|
|
|
current: pageIndex,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
onChange={(pag) => fetchData(pag.current, pag.pageSize)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 处理 — 对齐原型 regMessageProcess */}
|
|
|
|
|
|
<Modal
|
2026-08-17 14:17:45 +08:00
|
|
|
|
title={
|
|
|
|
|
|
detail
|
|
|
|
|
|
? `${processViewOnly ? "监管消息查看" : "监管消息处理"} - ${detail.noticeNo}`
|
|
|
|
|
|
: processViewOnly
|
|
|
|
|
|
? "监管消息查看"
|
|
|
|
|
|
: "监管消息处理"
|
|
|
|
|
|
}
|
2026-07-28 16:07:00 +08:00
|
|
|
|
open={processOpen}
|
|
|
|
|
|
onCancel={() => setProcessOpen(false)}
|
|
|
|
|
|
width={820}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
className="inst-modal"
|
|
|
|
|
|
footer={
|
2026-08-17 14:17:45 +08:00
|
|
|
|
processViewOnly ? (
|
|
|
|
|
|
<Button onClick={() => setProcessOpen(false)}>关闭</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button type="primary" loading={submitting} onClick={handleSubmit}>
|
|
|
|
|
|
提交
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)
|
2026-07-28 16:07:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{institutionInspNoticeGetLoading || !detail ? (
|
|
|
|
|
|
<div className="inst-loading">加载中...</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<ModalSection
|
|
|
|
|
|
title="处理要求"
|
|
|
|
|
|
desc="机构收到消息后直接填写说明并上传证明材料,完成后提交监管复核。"
|
|
|
|
|
|
/>
|
2026-08-17 14:17:45 +08:00
|
|
|
|
<Form
|
|
|
|
|
|
form={processForm}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
preserve={false}
|
|
|
|
|
|
scrollToFirstError
|
|
|
|
|
|
>
|
2026-07-28 16:07:00 +08:00
|
|
|
|
<div className="inst-form-grid">
|
|
|
|
|
|
<Form.Item name="noticeNo" label="关联消息编号">
|
|
|
|
|
|
<Input readOnly />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="processType" label="处理方式">
|
2026-08-17 14:17:45 +08:00
|
|
|
|
<Select options={PROCESS_TYPE_OPTIONS} disabled={processViewOnly} />
|
2026-07-28 16:07:00 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="processRemark"
|
|
|
|
|
|
label="处理说明"
|
|
|
|
|
|
className="inst-form-full"
|
|
|
|
|
|
>
|
2026-08-17 14:17:45 +08:00
|
|
|
|
<TextArea rows={3} disabled={processViewOnly} />
|
2026-07-28 16:07:00 +08:00
|
|
|
|
</Form.Item>
|
2026-08-17 14:17:45 +08:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={processViewOnly ? "附件" : "上传附件"}
|
|
|
|
|
|
className="inst-form-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{processViewOnly ? (
|
|
|
|
|
|
(() => {
|
|
|
|
|
|
const viewFiles = collectViewFiles(detail, materials);
|
|
|
|
|
|
return viewFiles.length ? (
|
|
|
|
|
|
viewFiles.map((m) => (
|
|
|
|
|
|
<a
|
|
|
|
|
|
key={m.id || m.fileUrl}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
window.open(resolveFileUrl(m.fileUrl), "_blank")
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: "block",
|
|
|
|
|
|
color: "#1677ff",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{materialFileName(m)}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span>暂无附件</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Upload
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
action={UPLOAD_ACTION}
|
|
|
|
|
|
headers={{
|
|
|
|
|
|
token: sessionStorage.getItem("token") || "",
|
|
|
|
|
|
}}
|
|
|
|
|
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
onChange={onFileUpload}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
<div className="inst-upload-tip">
|
|
|
|
|
|
支持 PDF、Word、Excel、图片,可选择多个文件。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-07-28 16:07:00 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 导出 — 对齐原型 regMessageExport */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="监管消息清单"
|
|
|
|
|
|
open={exportOpen}
|
|
|
|
|
|
onCancel={() => setExportOpen(false)}
|
|
|
|
|
|
width={560}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
className="inst-modal"
|
|
|
|
|
|
footer={
|
|
|
|
|
|
<Button type="primary" loading={exportLoading} onClick={handleExport}>
|
|
|
|
|
|
确认导出
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ModalSection
|
|
|
|
|
|
title="导出内容"
|
|
|
|
|
|
desc="包含消息编号、来源预警、来源字段、监管要求、办理期限、处理状态和办结结果。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</PageLayout>
|
|
|
|
|
|
</ConfigProvider>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Connect(
|
|
|
|
|
|
[NS_INSP_NOTICE],
|
|
|
|
|
|
true,
|
|
|
|
|
|
)(AntdTableFuncControl(InstitutionInspectionNotice));
|