safety-eval-service-frontend/src/pages/Container/MonitorManage/RiskCenter/index.js

404 lines
14 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 dayjs from "dayjs";
import { Button, Form, Table, Tag, Modal, Select, DatePicker, Input, Space, message } from "antd";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { tools } from "@cqsjjb/jjb-common-lib";
import AttachmentUpload from "~/components/AttachmentUpload";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_RISK_CENTER } from "~/enumerate/namespace";
import {
RISK_TYPE_MAP,
STAGE_MAP,
ORG_STATUS_MAP,
RISK_SUBTYPE_OPTIONS,
} from "~/enumerate/constant";
import "./index.less";
const { router } = tools;
/** 项目预警规则提示 */
const PROJ_RULES = [
"项目启动超期",
"关键节点超期",
"项目人员异常",
"从业告知异常",
"现场勘查异常",
"过程控制/归档异常",
];
/** 统计卡片样式主题 */
const CARD_THEMES = [
{ bg: "#fee2e2", color: "#dc2626" },
{ bg: "#fef3c7", color: "#d97706" },
{ bg: "#dbeafe", color: "#2563eb" },
{ bg: "#d1fae5", color: "#059669" },
];
const RiskCenter = (props) => {
const [form] = Form.useForm();
const [processForm] = Form.useForm();
const [processRecord, setProcessRecord] = useState(null);
const [processModalOpen, setProcessModalOpen] = useState(false);
const { riskCenter, queryRiskOverviewAction, queryRiskPageAction } = props;
const { overviewStats = [], riskPageList = [], riskPageTotal = 0, riskPageLoading } = riskCenter || {};
useEffect(() => {
queryRiskOverviewAction();
handleSearch();
}, []);
const handleSearch = () => {
queryRiskPageAction(router.query);
};
const handleReset = (values) => {
form.resetFields();
router.query = { ...values, current: 1, size: 10 };
handleSearch();
};
const handlePageChange = (page, size) => {
router.query = { ...router.query, current: page, size };
handleSearch()
};
/** 处理按钮 — 打开对应阶段的处理弹窗 */
const handleProcess = (record) => {
setProcessRecord(record);
if (record.stage === "未处理" || record.stage === "已发机构") {
processForm.setFieldsValue({
relatedId: record.id,
institution: record.riskObject,
deadline: dayjs().add(15, "day"),
opinion: "请核查来源字段与上传材料,形成监管处置记录。",
});
}
setProcessModalOpen(true);
};
/** 提交预警处置 */
const handleProcessSubmit = () => {
message.success("处理结果已提交");
setProcessModalOpen(false);
setProcessRecord(null);
processForm.resetFields();
};
/** 退回补正 */
const handleReturn = () => {
message.success("已退回机构补正");
setProcessModalOpen(false);
setProcessRecord(null);
};
/** 复核通过并闭环 */
const handleClose = () => {
message.success("预警已复核闭环");
setProcessModalOpen(false);
setProcessRecord(null);
};
const columns = [
{
title: "预警编号",
dataIndex: "id",
width: 160,
},
{
title: "预警大类",
dataIndex: "category",
width: 100,
render: (val) => {
const cfg = RISK_TYPE_MAP[val];
return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : val || "--";
},
},
{
title: "预警子类",
dataIndex: "subtype",
width: 140,
},
{
title: "风险对象",
dataIndex: "riskObject",
width: 200,
},
{
title: "来源字段",
dataIndex: "sourceFields",
width: 280,
render: (sources) => (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{(sources || []).map((s, i) => (
<span key={i} className="source-pill">
{s}
</span>
))}
</div>
),
},
{
title: "触发内容",
dataIndex: "content",
width: 320,
},
{
title: "处理阶段",
dataIndex: "stage",
width: 100,
render: (stage) => {
const cfg = STAGE_MAP[stage];
return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : "--";
},
},
{
title: "机构端状态",
dataIndex: "orgStatus",
width: 110,
render: (status) => {
const cfg = ORG_STATUS_MAP[status];
return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : "--";
},
},
{
title: "监管操作",
width: 100,
fixed: "right",
render: (_, record) => (
<Button type="primary" size="small" onClick={() => handleProcess(record)}>
处理
</Button>
),
},
];
return (
<PageLayout title="风险预警中心">
<div className="risk-center">
{/* 统计卡片 */}
<div className="stat-grid cols-4">
{overviewStats.map((stat, idx) => {
const theme = CARD_THEMES[idx % CARD_THEMES.length];
return (
<div className="stat-card" key={idx}>
<div className="stat-info">
<div className="stat-label">{stat.label}</div>
<div className="stat-value">{stat.value}</div>
<div className="stat-hint">{stat.hint}</div>
</div>
<div
className="stat-icon"
style={{ background: theme.bg, color: theme.color }}
>
{stat.label.charAt(0)}
</div>
</div>
);
})}
</div>
{/* 项目预警规则提示 */}
<div className="source-note">
<strong>项目预警规则</strong>
{PROJ_RULES.map((rule) => (
<span key={rule} className="source-pill">
{rule}
</span>
))}
</div>
{/* 搜索表单 */}
<SearchForm
style={{ marginBottom: 12 }}
form={form}
formLine={[
<Form.Item key="keyword" name="keyword">
<ControlWrapper.Input
label="关键词"
placeholder="机构/项目/人员/预警编号"
allowClear
/>
</Form.Item>,
<Form.Item key="riskType" name="type">
<ControlWrapper.Select
label="预警大类"
placeholder="全部"
allowClear
style={{ width: "100%" }}
options={[
{ label: "资质预警", value: "资质预警" },
{ label: "项目预警", value: "项目预警" },
]}
/>
</Form.Item>,
<Form.Item key="subtype" name="subtype">
<ControlWrapper.Select
label="预警子类"
placeholder="全部"
allowClear
style={{ width: "100%" }}
options={RISK_SUBTYPE_OPTIONS}
/>
</Form.Item>,
<Form.Item key="stage" name="stage">
<ControlWrapper.Select
label="处理阶段"
placeholder="全部"
allowClear
style={{ width: "100%" }}
options={[
{ label: "未处理", value: "未处理" },
{ label: "已发机构", value: "已发机构" },
{ label: "待复核", value: "待复核" },
{ label: "已闭环", value: "已闭环" },
]}
/>
</Form.Item>,
]}
onReset={handleReset}
onFinish={(values) => {
router.query = { ...values, current: 1, size: 10 };
handleSearch();
}}
/>
{/* 工具条 */}
<div className="risk-toolbar">
<div className="risk-toolbar-left">
<span className="risk-toolbar-desc">
资质预警沿用资质保持监控结果项目预警由项目计划人员从业告知现场记录和审核归档数据触发
</span>
</div>
<div className="risk-toolbar-right">
<Button className="btn-ghost" size="small">
导出预警
</Button>
</div>
</div>
{/* 数据表格 */}
<Table
rowKey="id"
columns={columns}
dataSource={riskPageList}
loading={riskPageLoading}
scroll={{ x: 1600, y: props.scrollY }}
pagination={{
total: riskPageTotal,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: Number(router.query.current) || 1,
pageSize: Number(router.query.size) || 10,
onChange: handlePageChange,
}}
/>
{/* 风险预警处置弹窗(未处理 / 已发机构) */}
{processRecord && (processRecord.stage === "未处理" || processRecord.stage === "已发机构") && (
<Modal
title={`风险预警处置 - ${processRecord.id}`}
open={processModalOpen}
onCancel={() => { setProcessModalOpen(false); setProcessRecord(null); }}
footer={
<Space>
<Button onClick={() => { setProcessModalOpen(false); setProcessRecord(null); }}>关闭</Button>
<Button type="primary" onClick={handleProcessSubmit}>提交</Button>
</Space>
}
width={760}
destroyOnHidden
>
<div style={{ padding: "0 0 8px", fontSize: 13, color: "#666" }}>
风险预警中心统一办理监管处置确认来源选择处置方式发送机构消息等待机构反馈复核并闭环
</div>
<Form form={processForm} layout="vertical" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0 16px" }} scrollToFirstError>
<Form.Item name="method" label="处置方式" rules={[{ required: true, message: "请选择处置方式" }]}>
<Select placeholder="请选择处置方式" options={[
{ label: "发送整改通知", value: "send_rectify" },
{ label: "发送核查函", value: "send_check" },
{ label: "退回补正", value: "return" },
{ label: "转办复核", value: "transfer" },
{ label: "标记闭环", value: "mark_closed" },
]} />
</Form.Item>
<Form.Item name="deadline" label="办理期限" rules={[{ required: true, message: "请选择办理期限" }]}>
<DatePicker style={{ width: "100%" }} />
</Form.Item>
<Form.Item name="relatedId" label="关联预警编号">
<Input disabled />
</Form.Item>
<Form.Item name="institution" label="接收机构">
<Input disabled />
</Form.Item>
<Form.Item name="opinion" label="监管意见" style={{ gridColumn: "1 / -1" }}>
<Input.TextArea rows={3} />
</Form.Item>
<AttachmentUpload
name="attachments"
label="监管附件"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
extra="可上传核查材料、监管意见附件或其他处置依据。"
/>
</Form>
</Modal>
)}
{/* 机构反馈复核弹窗(待复核 / 已闭环) */}
{processRecord && (processRecord.stage === "待复核" || processRecord.stage === "已闭环") && (
<Modal
title={`机构反馈复核 - ${processRecord.id}`}
open={processModalOpen}
onCancel={() => { setProcessModalOpen(false); setProcessRecord(null); }}
footer={
<Space>
<Button onClick={() => { setProcessModalOpen(false); setProcessRecord(null); }}>关闭</Button>
<Button onClick={handleReturn}>退回补正</Button>
<Button type="primary" onClick={handleClose}>复核通过并闭环</Button>
</Space>
}
width={760}
destroyOnHidden
>
<div style={{ padding: "0 0 8px", fontSize: 13, color: "#666" }}>
机构端提交的说明和附件进入本环节复核监管人员可通过退回补正或办结
</div>
<Table
rowKey="name"
dataSource={[
{ name: "整改说明", content: "已说明问题原因和处理措施", status: "待复核", source: "机构端消息.整改说明" },
{ name: "证明附件", content: "机构上传的证明材料", status: "待复核", source: "机构端消息.附件" },
]}
columns={[
{ title: "材料项", dataIndex: "name", width: 100 },
{ title: "机构提交内容", dataIndex: "content", width: 200 },
{ title: "复核状态", dataIndex: "status", width: 100 },
{ title: "来源", dataIndex: "source" },
]}
pagination={false}
size="small"
/>
<div style={{ marginTop: 16 }}>
<AttachmentUpload
name="reviewAttachments"
label="复核附件"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
extra="可上传复核意见、核查记录或办结依据。"
/>
</div>
</Modal>
)}
</div>
</PageLayout>
);
};
export default Connect(
[NS_RISK_CENTER],
true,
)(AntdTableFuncControl(RiskCenter));