feat
parent
4cb916cf5c
commit
1d5d508292
|
|
@ -35,4 +35,10 @@ export const equipInfoRemove = declareRequest(
|
|||
export const equipInfoToggleStatus = declareRequest(
|
||||
"equipInfoLoading",
|
||||
"Post > @/safetyEval/org-equipment/modify",
|
||||
);
|
||||
|
||||
export const queryDeviceTypeOptions = declareRequest(
|
||||
"equipInfoLoading",
|
||||
"Get > /safetyEval/org-business-scope-device-ref",
|
||||
"deviceTypeOptions: [] | res.data || []",
|
||||
);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { apiPostDelete, safeAction } from "../../utils/enterpriseInfo/http";
|
||||
|
||||
|
||||
export const staffChangeLogStaffList = declareRequest(
|
||||
"staffChangeLogLoading",
|
||||
|
|
@ -13,10 +13,7 @@ export const staffChangeLogList = declareRequest(
|
|||
"staffChangeLogChangeList: [] | res.data || [] & staffChangeLogChangeTotal: 0 | res.total || 0",
|
||||
);
|
||||
|
||||
export const staffChangeLogRemove = declareRequest(
|
||||
"staffChangeLogLoading",
|
||||
safeAction(async ({ id }) => apiPostDelete("/safetyEval/org-personnel-change/delete", id)),
|
||||
);
|
||||
|
||||
|
||||
export const staffResignationAudit = declareRequest(
|
||||
"staffChangeLogLoading",
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ export default function StaffViewModal({
|
|||
<Descriptions.Item label="是否注册安全工程师">
|
||||
{REGISTER_ENGINEER_MAP[info.registerEngineerFlag] ?? "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="是否技术负责人">
|
||||
{info.technicalDirectorFlag === true ? "是" : info.technicalDirectorFlag === false ? "否" : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="是否过程控制负责人">
|
||||
{info.processControlLeaderFlag === true ? "是" : info.processControlLeaderFlag === false ? "否" : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证号">
|
||||
{info.idCardNo}
|
||||
</Descriptions.Item>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
|||
import { NS_EQUIP_INFO } from "~/enumerate/namespace";
|
||||
import { positiveNumberRule } from "~/utils/validators";
|
||||
import { toEquipPageQuery } from "../equipPageQuery";
|
||||
import { QUALIFICATION_INDUSTRY_OPTIONS } from "~/enumerate/enterpriseOptions";
|
||||
|
||||
const { router } = tools;
|
||||
const DUAL_CHANNEL_MAP = { 1: "是", 2: "否" };
|
||||
|
|
@ -84,7 +85,6 @@ function EquipInfoPage(props) {
|
|||
message.success("操作成功");
|
||||
handleSearch();
|
||||
} else {
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -119,29 +119,14 @@ function EquipInfoPage(props) {
|
|||
maxLength={30}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="instrumentType" name="instrumentType">
|
||||
<Form.Item key="industryCode" name="industryCode">
|
||||
<ControlWrapper.Select
|
||||
label="仪器类型"
|
||||
label="业务范围"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select.Option value="分析仪器">分析仪器</Select.Option>
|
||||
<Select.Option value="检测仪器">检测仪器</Select.Option>
|
||||
<Select.Option value="采样设备">采样设备</Select.Option>
|
||||
<Select.Option value="物理仪器">物理仪器</Select.Option>
|
||||
</ControlWrapper.Select>
|
||||
</Form.Item>,
|
||||
<Form.Item key="deviceType" name="deviceType">
|
||||
<ControlWrapper.Select
|
||||
label="设备类型"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select.Option value="固定式">固定式</Select.Option>
|
||||
<Select.Option value="便携式">便携式</Select.Option>
|
||||
</ControlWrapper.Select>
|
||||
options={QUALIFICATION_INDUSTRY_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
]}
|
||||
onReset={(value) => {
|
||||
|
|
@ -162,9 +147,16 @@ function EquipInfoPage(props) {
|
|||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{ title: "设备型号", dataIndex: "deviceModel", ellipsis: true, },
|
||||
{ title: "仪器类型", dataIndex: "instrumentTypeName" },
|
||||
{ title: "设备类型", dataIndex: "deviceTypeName" },
|
||||
{ title: "设备型号", dataIndex: "deviceModel", ellipsis: true },
|
||||
{
|
||||
title: "业务范围",
|
||||
dataIndex: "industryCode",
|
||||
render: (v) =>
|
||||
QUALIFICATION_INDUSTRY_OPTIONS.find((o) => o.value === v)
|
||||
?.label ??
|
||||
(v || "-"),
|
||||
},
|
||||
{ title: "设备类型", dataIndex: "deviceTypeCode" },
|
||||
{
|
||||
title: "厂家",
|
||||
dataIndex: "manufacturer",
|
||||
|
|
@ -231,7 +223,7 @@ function EquipInfoPage(props) {
|
|||
},
|
||||
]}
|
||||
dataSource={dataSource}
|
||||
scroll={{ y: props.scrollY, x: 1600 }}
|
||||
scroll={{ y: props.scrollY, x: 1600 }}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
|
|
@ -254,6 +246,7 @@ function EquipInfoPage(props) {
|
|||
equipInfoGet={props.equipInfoGet}
|
||||
equipInfoAdd={props.equipInfoAdd}
|
||||
equipInfoEdit={props.equipInfoEdit}
|
||||
queryDeviceTypeOptions={props.queryDeviceTypeOptions}
|
||||
onCancel={() => {
|
||||
setFormModalOpen(false);
|
||||
setCurrentId("");
|
||||
|
|
@ -282,12 +275,46 @@ function EquipFormModal({
|
|||
equipInfoGet,
|
||||
equipInfoAdd,
|
||||
equipInfoEdit,
|
||||
queryDeviceTypeOptions,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [deviceTypeOptions, setDeviceTypeOptions] = useState([]);
|
||||
const [deviceTypeLoading, setDeviceTypeLoading] = useState(false);
|
||||
const industryCode = Form.useWatch("industryCode", form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!industryCode) {
|
||||
setDeviceTypeOptions([]);
|
||||
form.resetFields(["deviceTypeCode"]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setDeviceTypeLoading(true);
|
||||
queryDeviceTypeOptions({ industryCode, pageSize: 999 })
|
||||
.then((res) => {
|
||||
if (cancelled) return;
|
||||
const list = res?.data || [];
|
||||
const unique = Array.from(
|
||||
new Map(list.map((item) => [item.deviceCode, item])).values(),
|
||||
);
|
||||
setDeviceTypeOptions(
|
||||
unique.map((item) => ({
|
||||
label: item.deviceName,
|
||||
value: item.deviceCode,
|
||||
})),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setDeviceTypeLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [industryCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
|
@ -328,7 +355,6 @@ function EquipFormModal({
|
|||
handleCancel();
|
||||
onSuccess();
|
||||
} else {
|
||||
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
|
|
@ -368,28 +394,32 @@ function EquipFormModal({
|
|||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="instrumentTypeName"
|
||||
label="仪器分类"
|
||||
rules={[{ required: true, message: "请选择仪器分类" }]}
|
||||
name="industryCode"
|
||||
label="业务范围"
|
||||
rules={[{ required: true, message: "请选择业务范围" }]}
|
||||
>
|
||||
<Select placeholder="请选择仪器分类">
|
||||
<Select.Option value="分析仪器">分析仪器</Select.Option>
|
||||
<Select.Option value="检测仪器">检测仪器</Select.Option>
|
||||
<Select.Option value="采样设备">采样设备</Select.Option>
|
||||
<Select.Option value="物理仪器">物理仪器</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="请选择业务范围"
|
||||
options={QUALIFICATION_INDUSTRY_OPTIONS}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="deviceTypeName"
|
||||
name="deviceTypeCode"
|
||||
label="设备类型"
|
||||
rules={[{ required: true, message: "请选择设备类型" }]}
|
||||
>
|
||||
<Select placeholder="请选择设备类型">
|
||||
<Select.Option value="固定式">固定式</Select.Option>
|
||||
<Select.Option value="便携式">便携式</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="请先选择业务范围"
|
||||
loading={deviceTypeLoading}
|
||||
options={deviceTypeOptions}
|
||||
disabled={!industryCode}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
|
|
@ -403,7 +433,11 @@ function EquipFormModal({
|
|||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="flowDesc" label="设备流量说明">
|
||||
<Input placeholder="请输入设备流量说明" maxLength={100} showCount />
|
||||
<Input
|
||||
placeholder="请输入设备流量说明"
|
||||
maxLength={100}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
|
|
@ -413,7 +447,6 @@ function EquipFormModal({
|
|||
rules={[positiveNumberRule("最小流量", false)]}
|
||||
>
|
||||
<InputNumber
|
||||
|
||||
style={{ width: "100%" }}
|
||||
placeholder="请输入最小流量"
|
||||
/>
|
||||
|
|
@ -426,7 +459,6 @@ function EquipFormModal({
|
|||
rules={[positiveNumberRule("最大流量", false)]}
|
||||
>
|
||||
<InputNumber
|
||||
|
||||
style={{ width: "100%" }}
|
||||
placeholder="请输入最大流量"
|
||||
/>
|
||||
|
|
@ -517,11 +549,14 @@ function EquipViewModal({ open, currentId, equipInfoGet, onCancel }) {
|
|||
<Descriptions.Item label="设备型号">
|
||||
{info.deviceModel}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="仪器分类">
|
||||
{info.instrumentTypeName}
|
||||
<Descriptions.Item label="业务范围">
|
||||
{QUALIFICATION_INDUSTRY_OPTIONS.find(
|
||||
(o) => o.value === info.industryCode,
|
||||
)?.label ??
|
||||
(info.industryCode || "-")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">
|
||||
{info.deviceTypeName}
|
||||
{info.deviceTypeCode || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="厂家">{info.manufacturer}</Descriptions.Item>
|
||||
<Descriptions.Item label="设备流量说明" span={2}>
|
||||
|
|
|
|||
|
|
@ -710,6 +710,16 @@ function StaffFormModal({
|
|||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="technicalDirectorFlag" label="是否技术负责人">
|
||||
<Select placeholder="请选择" allowClear options={[{ label: "是", value: true }, { label: "否", value: false }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="processControlLeaderFlag" label="是否过程控制负责人">
|
||||
<Select placeholder="请选择" allowClear options={[{ label: "是", value: true }, { label: "否", value: false }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="publications"
|
||||
|
|
|
|||
|
|
@ -106,11 +106,7 @@ const menuItems = [
|
|||
label: "备案变更管理",
|
||||
icon: <SwapOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/safetyEval/container/QualificationReview/QualMonitor",
|
||||
label: "资质保持监控",
|
||||
icon: <BarChartOutlined />,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -179,6 +175,23 @@ const menuItems = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "/safetyEval/container/MonitorManage",
|
||||
label: "监督管理",
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "/safetyEval/container/MonitorManage/QualMonitor",
|
||||
label: "资质保持监控",
|
||||
icon: <BarChartOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/safetyEval/container/MonitorManage/RiskCenter",
|
||||
label: "风险预警中心",
|
||||
icon: <ExperimentOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "/safetyEval/container/test",
|
||||
label: "测试",
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ const QualMonitor = (props) => {
|
|||
{
|
||||
label: "条件预警机构",
|
||||
value: stats.warning,
|
||||
hint: "人员比例不满足要求",
|
||||
hint: "人员比例、属地材料待核验",
|
||||
color: "#d97706",
|
||||
iconBg: "#fef3c7",
|
||||
iconColor: "#d97706",
|
||||
|
|
@ -335,10 +335,8 @@ const QualMonitor = (props) => {
|
|||
|
||||
{/* 资质条件来源说明 */}
|
||||
<div className="source-note">
|
||||
<strong>资质条件来源:</strong>
|
||||
依据安全评价机构资质条件,将机构备案信息拆为固定资产不少于 800
|
||||
万元、工作场所不少于 1000 平方米、档案室不少于 100 平方米、
|
||||
专职技术人员数量及比例、技术负责人/过程控制负责人、内部制度、法定代表人承诺、公开网站、三年重大违法失信记录、注册地属地满足情况等字段。
|
||||
<strong>页面定位:</strong>
|
||||
资质保持监控是监管信息看板,展示机构具备哪些资质、资质条件是否保持、每个结论来自哪些字段。系统命中异常后自动生成风险消息,发函、整改、机构反馈、复核闭环等办理流程全部在“风险预警中心”完成。
|
||||
</div>
|
||||
|
||||
{/* 搜索表单 */}
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button, Form, Table, Tag } 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 { tools } from "@cqsjjb/jjb-common-lib";
|
||||
import "./index.less";
|
||||
|
||||
const { router } = tools;
|
||||
|
||||
/** 预警大类配置 */
|
||||
const RISK_TYPE_MAP = {
|
||||
qual: { label: "资质预警", color: "error" },
|
||||
proj: { label: "项目预警", color: "warning" },
|
||||
};
|
||||
|
||||
/** 处理阶段配置 */
|
||||
const STAGE_MAP = {
|
||||
untreated: { label: "未处理", color: "error" },
|
||||
sent: { label: "已发机构", color: "warning" },
|
||||
review: { label: "待复核", color: "processing" },
|
||||
closed: { label: "已闭环", color: "success" },
|
||||
};
|
||||
|
||||
/** 机构端状态配置 */
|
||||
const ORG_STATUS_MAP = {
|
||||
unsent: { label: "未发送", color: "error" },
|
||||
unread: { label: "机构未读", color: "warning" },
|
||||
processing: { label: "处理中", color: "processing" },
|
||||
submitted: { label: "已提交材料", color: "processing" },
|
||||
done: { label: "整改完成", color: "success" },
|
||||
};
|
||||
|
||||
/** 预警子类选项 */
|
||||
const RISK_SUBTYPE_OPTIONS = [
|
||||
{ label: "资质条件保持异常", value: "qual_condition" },
|
||||
{ label: "人员比例异常", value: "qual_person_rate" },
|
||||
{ label: "属地材料待核验", value: "qual_area" },
|
||||
{ label: "项目启动超期", value: "proj_start" },
|
||||
{ label: "关键节点超期", value: "proj_node" },
|
||||
{ label: "项目人员异常", value: "proj_person" },
|
||||
{ label: "从业告知异常", value: "proj_notify" },
|
||||
{ label: "现场勘查异常", value: "proj_site" },
|
||||
{ label: "过程控制/归档异常", value: "proj_archive" },
|
||||
];
|
||||
|
||||
/** 项目预警规则提示 */
|
||||
const PROJ_RULES = [
|
||||
"项目启动超期",
|
||||
"关键节点超期",
|
||||
"项目人员异常",
|
||||
"从业告知异常",
|
||||
"现场勘查异常",
|
||||
"过程控制/归档异常",
|
||||
];
|
||||
|
||||
/** mock 数据 */
|
||||
const MOCK_DATA = [
|
||||
{
|
||||
id: "W-QUAL-2026-012",
|
||||
type: "qual",
|
||||
subtype: "qual_condition",
|
||||
target: "重庆渝安风险评估中心",
|
||||
sources: ["财务材料.固定资产", "场所核验.建筑面积", "人员库.专职人数"],
|
||||
content: "固定资产、场所面积、档案室面积和专职人员数量不满足保持条件。",
|
||||
stage: "untreated",
|
||||
orgStatus: "unsent",
|
||||
},
|
||||
{
|
||||
id: "W-QUAL-2026-011",
|
||||
type: "qual",
|
||||
subtype: "qual_person_rate",
|
||||
target: "重庆恒安安全评价有限公司",
|
||||
sources: ["人员库.职称等级", "人员库.注册安全工程师"],
|
||||
content: "高级职称比例 26%,合计比例 59%,低于资质保持条件。",
|
||||
stage: "sent",
|
||||
orgStatus: "unread",
|
||||
},
|
||||
{
|
||||
id: "W-QUAL-2026-013",
|
||||
type: "qual",
|
||||
subtype: "qual_area",
|
||||
target: "北京中安评价中心重庆分公司",
|
||||
sources: ["本市社保.缴纳人数", "场所核验.租赁合同"],
|
||||
content: "机构已提交本地场所和人员说明,等待监管复核。",
|
||||
stage: "review",
|
||||
orgStatus: "submitted",
|
||||
},
|
||||
{
|
||||
id: "W-PROJ-2026-021",
|
||||
type: "proj",
|
||||
subtype: "proj_start",
|
||||
target: "华安化工年度安全现状评价",
|
||||
sources: ["客户信息.评价到期日", "项目库.新建时间"],
|
||||
content: "评价周期已到期 14 天,系统内尚未建立新一轮评价项目。",
|
||||
stage: "untreated",
|
||||
orgStatus: "unsent",
|
||||
},
|
||||
{
|
||||
id: "W-PROJ-2026-018",
|
||||
type: "proj",
|
||||
subtype: "proj_node",
|
||||
target: "安环检测年度安全现状评价",
|
||||
sources: ["项目计划.节点期限", "流程记录.当前节点"],
|
||||
content: "项目停留在过程控制环节 12 天,已超过计划完成时间。",
|
||||
stage: "sent",
|
||||
orgStatus: "processing",
|
||||
},
|
||||
{
|
||||
id: "W-PROJ-2026-015",
|
||||
type: "proj",
|
||||
subtype: "proj_person",
|
||||
target: "鼎信建设安全验收评价",
|
||||
sources: ["项目组.项目负责人", "人员库.在职状态"],
|
||||
content: "项目负责人已提交离职申请,项目尚未完成负责人变更。",
|
||||
stage: "review",
|
||||
orgStatus: "submitted",
|
||||
},
|
||||
{
|
||||
id: "W-PROJ-2026-009",
|
||||
type: "proj",
|
||||
subtype: "proj_site",
|
||||
target: "华安化工园区安全预评价",
|
||||
sources: ["现场勘查.签到签退", "项目组.参与人员"],
|
||||
content: "现场勘查仅有签到记录,无签退记录和现场照片。",
|
||||
stage: "closed",
|
||||
orgStatus: "done",
|
||||
},
|
||||
];
|
||||
|
||||
const RiskCenter = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [data] = useState(MOCK_DATA);
|
||||
|
||||
/** 统计卡片数据 */
|
||||
const stats = {
|
||||
qualRisk: data.filter((i) => i.type === "qual").length,
|
||||
projRisk: data.filter((i) => i.type === "proj").length,
|
||||
pendingFeedback: data.filter((i) => i.orgStatus === "submitted").length,
|
||||
todayClosed: data.filter((i) => i.stage === "closed").length,
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 接口搜索
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "预警编号",
|
||||
dataIndex: "id",
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: "预警大类",
|
||||
dataIndex: "type",
|
||||
width: 100,
|
||||
render: (type) => {
|
||||
const cfg = RISK_TYPE_MAP[type];
|
||||
return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : "--";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "预警子类",
|
||||
dataIndex: "subtype",
|
||||
width: 140,
|
||||
render: (subtype) => {
|
||||
const opt = RISK_SUBTYPE_OPTIONS.find((o) => o.value === subtype);
|
||||
return opt?.label || "--";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "风险对象",
|
||||
dataIndex: "target",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "来源字段",
|
||||
dataIndex: "sources",
|
||||
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: () => (
|
||||
<Button type="primary" size="small">
|
||||
处理
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout title="风险预警中心">
|
||||
<div className="risk-center">
|
||||
{/* 统计卡片 */}
|
||||
<div className="stat-grid cols-4">
|
||||
<div className="stat-card">
|
||||
<div className="stat-info">
|
||||
<div className="stat-label">资质预警</div>
|
||||
<div className="stat-value stat-value--high">
|
||||
{stats.qualRisk}
|
||||
</div>
|
||||
<div className="stat-hint">
|
||||
条件保持、人员比例、属地材料
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="stat-icon"
|
||||
style={{ background: "#fee2e2", color: "#dc2626" }}
|
||||
>
|
||||
资
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-info">
|
||||
<div className="stat-label">项目预警</div>
|
||||
<div className="stat-value stat-value--mid">
|
||||
{stats.projRisk}
|
||||
</div>
|
||||
<div className="stat-hint">
|
||||
进度、人员、告知、勘查、质控
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="stat-icon"
|
||||
style={{ background: "#fef3c7", color: "#d97706" }}
|
||||
>
|
||||
项
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-info">
|
||||
<div className="stat-label">待机构反馈</div>
|
||||
<div className="stat-value stat-value--mid">
|
||||
{stats.pendingFeedback}
|
||||
</div>
|
||||
<div className="stat-hint">整改说明或证明材料</div>
|
||||
</div>
|
||||
<div
|
||||
className="stat-icon"
|
||||
style={{ background: "#dbeafe", color: "#2563eb" }}
|
||||
>
|
||||
反
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-info">
|
||||
<div className="stat-label">今日闭环</div>
|
||||
<div className="stat-value stat-value--ok">
|
||||
{stats.todayClosed}
|
||||
</div>
|
||||
<div className="stat-hint">监管复核已完成</div>
|
||||
</div>
|
||||
<div
|
||||
className="stat-icon"
|
||||
style={{ background: "#d1fae5", color: "#059669" }}
|
||||
>
|
||||
结
|
||||
</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="riskType">
|
||||
<ControlWrapper.Select
|
||||
label="预警大类"
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
options={[
|
||||
{ label: "资质预警", value: "qual" },
|
||||
{ label: "项目预警", value: "proj" },
|
||||
]}
|
||||
/>
|
||||
</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: "untreated" },
|
||||
{ label: "已发机构", value: "sent" },
|
||||
{ label: "待复核", value: "review" },
|
||||
{ label: "已闭环", value: "closed" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>,
|
||||
]}
|
||||
onReset={handleReset}
|
||||
onFinish={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={data}
|
||||
scroll={{ x: 1600 }}
|
||||
pagination={{
|
||||
total: data.length,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
current: router.query.current || 1,
|
||||
pageSize: router.query.size || 10,
|
||||
onChange: (page, size) => {
|
||||
router.query = { ...router.query, current: page, size };
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default RiskCenter;
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
.risk-center {
|
||||
// 统计卡片网格
|
||||
.stat-grid.cols-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.stat-info {
|
||||
.stat-label {
|
||||
font-size: 0.82rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.stat-value--high {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.stat-value--mid {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.stat-value--ok {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.stat-hint {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 来源说明/规则提示
|
||||
.source-note {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.82rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
strong {
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
// 来源字段药丸
|
||||
.source-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 0.68rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 工具条
|
||||
.risk-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.risk-toolbar-left {
|
||||
.risk-toolbar-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.risk-toolbar-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
import React from "react";
|
||||
|
||||
function MonitorManage(props) {
|
||||
return (
|
||||
|
||||
props.children
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default MonitorManage;
|
||||
|
|
@ -3,46 +3,7 @@
|
|||
import { asId, isIdFieldName, normalizeQueryIds } from "./idUtil";
|
||||
import { withOrgId } from "./orgContext";
|
||||
|
||||
function parseAttachmentUrls(urls, defaultName = "附件.pdf") {
|
||||
if (!urls) {
|
||||
return [];
|
||||
}
|
||||
const raw = String(urls).trim();
|
||||
|
||||
// JSON 数组字符串:如 '[{"url":"...","uid":"...","name":"...","status":"done"}]'
|
||||
if (raw.startsWith("[") && raw.endsWith("]")) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
.filter((item) => item?.url)
|
||||
.map((item) => ({
|
||||
name: item.name || `${defaultName.replace(".pdf", "")}1`,
|
||||
fileName:
|
||||
item.fileName ||
|
||||
item.name ||
|
||||
`${defaultName.replace(".pdf", "")}1`,
|
||||
url: item.url,
|
||||
uid: item.uid || undefined,
|
||||
status: item.status || "done",
|
||||
}));
|
||||
}
|
||||
} catch (_) {
|
||||
// JSON 解析失败,回退到逗号分隔处理
|
||||
}
|
||||
}
|
||||
|
||||
// 旧格式:逗号分隔的 URL 字符串
|
||||
return raw
|
||||
.split(",")
|
||||
.map((url) => url.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => ({
|
||||
name: `${defaultName.replace(".pdf", "")}${index + 1}.pdf`,
|
||||
fileName: `${defaultName.replace(".pdf", "")}${index + 1}.pdf`,
|
||||
url,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 分页查询参数:子表 org_id → org_info.id;机构信息管理 getInfo 不走此方法 */
|
||||
export function toPageQuery(params = {}, fieldMap = {}) {
|
||||
|
|
|
|||
|
|
@ -64,18 +64,7 @@ export async function apiPostDelete(path, id, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
export function safePageResult(mapper) {
|
||||
return async (params) => {
|
||||
try {
|
||||
const res = await mapper(params);
|
||||
return res;
|
||||
}
|
||||
catch (err) {
|
||||
console.warn("[enterpriseInfo/safePageResult]", err);
|
||||
return { success: false, data: [], totalCount: 0 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function safeAction(mapper) {
|
||||
return async (params) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue