404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
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));
|