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} destroyOnHidden className="inst-modal" footer={ processViewOnly ? ( ) : ( ) } > {institutionInspNoticeGetLoading || !detail ? (
加载中...
) : ( <>