From 1d5d508292382005f27d861bfb7f01091fe4c47b Mon Sep 17 00:00:00 2001 From: tangjie <122778500@qq.com> Date: Wed, 15 Jul 2026 14:26:05 +0800 Subject: [PATCH] feat --- src/api/equipInfo/index.js | 6 + src/api/staffChangeLog/index.js | 7 +- src/components/StaffViewModal/index.js | 6 + .../EnterpriseInfo/EquipInfo/index.js | 125 ++++-- .../PersonnelInfo/List/index.js | 10 + src/pages/Container/Layout/menuConfig.js | 23 +- .../QualMonitor/index.js | 8 +- .../QualMonitor/index.less | 0 .../MonitorManage/RiskCenter/index.js | 399 ++++++++++++++++++ .../MonitorManage/RiskCenter/index.less | 125 ++++++ src/pages/Container/MonitorManage/index.js | 12 + src/utils/enterpriseInfo/adapter.js | 39 -- src/utils/enterpriseInfo/http.js | 13 +- 13 files changed, 662 insertions(+), 111 deletions(-) rename src/pages/Container/{QualificationReview => MonitorManage}/QualMonitor/index.js (96%) rename src/pages/Container/{QualificationReview => MonitorManage}/QualMonitor/index.less (100%) create mode 100644 src/pages/Container/MonitorManage/RiskCenter/index.js create mode 100644 src/pages/Container/MonitorManage/RiskCenter/index.less create mode 100644 src/pages/Container/MonitorManage/index.js diff --git a/src/api/equipInfo/index.js b/src/api/equipInfo/index.js index 468d891..58936b1 100644 --- a/src/api/equipInfo/index.js +++ b/src/api/equipInfo/index.js @@ -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 || []", ); \ No newline at end of file diff --git a/src/api/staffChangeLog/index.js b/src/api/staffChangeLog/index.js index dcb51fa..ea2fe61 100644 --- a/src/api/staffChangeLog/index.js +++ b/src/api/staffChangeLog/index.js @@ -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", diff --git a/src/components/StaffViewModal/index.js b/src/components/StaffViewModal/index.js index 30c9fe0..be671ff 100644 --- a/src/components/StaffViewModal/index.js +++ b/src/components/StaffViewModal/index.js @@ -158,6 +158,12 @@ export default function StaffViewModal({ {REGISTER_ENGINEER_MAP[info.registerEngineerFlag] ?? "-"} + + {info.technicalDirectorFlag === true ? "是" : info.technicalDirectorFlag === false ? "否" : "-"} + + + {info.processControlLeaderFlag === true ? "是" : info.processControlLeaderFlag === false ? "否" : "-"} + {info.idCardNo} diff --git a/src/pages/Container/EnterpriseInfo/EquipInfo/index.js b/src/pages/Container/EnterpriseInfo/EquipInfo/index.js index a28693a..62222ff 100644 --- a/src/pages/Container/EnterpriseInfo/EquipInfo/index.js +++ b/src/pages/Container/EnterpriseInfo/EquipInfo/index.js @@ -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} /> , - + - 分析仪器 - 检测仪器 - 采样设备 - 物理仪器 - - , - - - 固定式 - 便携式 - + options={QUALIFICATION_INDUSTRY_OPTIONS} + /> , ]} 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({ - + - 固定式 - 便携式 - + + @@ -413,7 +447,6 @@ function EquipFormModal({ rules={[positiveNumberRule("最小流量", false)]} > @@ -426,7 +459,6 @@ function EquipFormModal({ rules={[positiveNumberRule("最大流量", false)]} > @@ -517,11 +549,14 @@ function EquipViewModal({ open, currentId, equipInfoGet, onCancel }) { {info.deviceModel} - - {info.instrumentTypeName} + + {QUALIFICATION_INDUSTRY_OPTIONS.find( + (o) => o.value === info.industryCode, + )?.label ?? + (info.industryCode || "-")} - {info.deviceTypeName} + {info.deviceTypeCode || "-"} {info.manufacturer} diff --git a/src/pages/Container/EnterpriseInfo/PersonnelInfo/List/index.js b/src/pages/Container/EnterpriseInfo/PersonnelInfo/List/index.js index 31b5813..3940fb5 100644 --- a/src/pages/Container/EnterpriseInfo/PersonnelInfo/List/index.js +++ b/src/pages/Container/EnterpriseInfo/PersonnelInfo/List/index.js @@ -710,6 +710,16 @@ function StaffFormModal({ /> + + + + + , }, - { - key: "/safetyEval/container/QualificationReview/QualMonitor", - label: "资质保持监控", - icon: , - }, + ], }, ] @@ -179,6 +175,23 @@ const menuItems = [ }, ], }, + { + key: "/safetyEval/container/MonitorManage", + label: "监督管理", + icon: , + children: [ + { + key: "/safetyEval/container/MonitorManage/QualMonitor", + label: "资质保持监控", + icon: , + }, + { + key: "/safetyEval/container/MonitorManage/RiskCenter", + label: "风险预警中心", + icon: , + }, + ], + }, { key: "/safetyEval/container/test", label: "测试", diff --git a/src/pages/Container/QualificationReview/QualMonitor/index.js b/src/pages/Container/MonitorManage/QualMonitor/index.js similarity index 96% rename from src/pages/Container/QualificationReview/QualMonitor/index.js rename to src/pages/Container/MonitorManage/QualMonitor/index.js index 77ca941..d3f4ab8 100644 --- a/src/pages/Container/QualificationReview/QualMonitor/index.js +++ b/src/pages/Container/MonitorManage/QualMonitor/index.js @@ -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) => { {/* 资质条件来源说明 */}
- 资质条件来源: - 依据安全评价机构资质条件,将机构备案信息拆为固定资产不少于 800 - 万元、工作场所不少于 1000 平方米、档案室不少于 100 平方米、 - 专职技术人员数量及比例、技术负责人/过程控制负责人、内部制度、法定代表人承诺、公开网站、三年重大违法失信记录、注册地属地满足情况等字段。 + 页面定位: + 资质保持监控是监管信息看板,展示机构具备哪些资质、资质条件是否保持、每个结论来自哪些字段。系统命中异常后自动生成风险消息,发函、整改、机构反馈、复核闭环等办理流程全部在“风险预警中心”完成。
{/* 搜索表单 */} diff --git a/src/pages/Container/QualificationReview/QualMonitor/index.less b/src/pages/Container/MonitorManage/QualMonitor/index.less similarity index 100% rename from src/pages/Container/QualificationReview/QualMonitor/index.less rename to src/pages/Container/MonitorManage/QualMonitor/index.less diff --git a/src/pages/Container/MonitorManage/RiskCenter/index.js b/src/pages/Container/MonitorManage/RiskCenter/index.js new file mode 100644 index 0000000..5675798 --- /dev/null +++ b/src/pages/Container/MonitorManage/RiskCenter/index.js @@ -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 ? {cfg.label} : "--"; + }, + }, + { + 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) => ( +
+ {(sources || []).map((s, i) => ( + + {s} + + ))} +
+ ), + }, + { + title: "触发内容", + dataIndex: "content", + width: 320, + }, + { + title: "处理阶段", + dataIndex: "stage", + width: 100, + render: (stage) => { + const cfg = STAGE_MAP[stage]; + return cfg ? {cfg.label} : "--"; + }, + }, + { + title: "机构端状态", + dataIndex: "orgStatus", + width: 110, + render: (status) => { + const cfg = ORG_STATUS_MAP[status]; + return cfg ? {cfg.label} : "--"; + }, + }, + { + title: "监管操作", + width: 100, + fixed: "right", + render: () => ( + + ), + }, + ]; + + return ( + +
+ {/* 统计卡片 */} +
+
+
+
资质预警
+
+ {stats.qualRisk} +
+
+ 条件保持、人员比例、属地材料 +
+
+
+ 资 +
+
+
+
+
项目预警
+
+ {stats.projRisk} +
+
+ 进度、人员、告知、勘查、质控 +
+
+
+ 项 +
+
+
+
+
待机构反馈
+
+ {stats.pendingFeedback} +
+
整改说明或证明材料
+
+
+ 反 +
+
+
+
+
今日闭环
+
+ {stats.todayClosed} +
+
监管复核已完成
+
+
+ 结 +
+
+
+ + {/* 项目预警规则提示 */} +
+ 项目预警规则: + {PROJ_RULES.map((rule) => ( + + {rule} + + ))} +
+ + {/* 搜索表单 */} + + + , + + + , + + + , + + + , + ]} + onReset={handleReset} + onFinish={handleSearch} + /> + + {/* 工具条 */} +
+
+ + 资质预警沿用资质保持监控结果;项目预警由项目计划、人员、从业告知、现场记录和审核归档数据触发。 + +
+
+ +
+
+ + {/* 数据表格 */} + `共 ${total} 条`, + current: router.query.current || 1, + pageSize: router.query.size || 10, + onChange: (page, size) => { + router.query = { ...router.query, current: page, size }; + }, + }} + /> + + + ); +}; + +export default RiskCenter; diff --git a/src/pages/Container/MonitorManage/RiskCenter/index.less b/src/pages/Container/MonitorManage/RiskCenter/index.less new file mode 100644 index 0000000..572f4cf --- /dev/null +++ b/src/pages/Container/MonitorManage/RiskCenter/index.less @@ -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; + } + } +} diff --git a/src/pages/Container/MonitorManage/index.js b/src/pages/Container/MonitorManage/index.js new file mode 100644 index 0000000..f568631 --- /dev/null +++ b/src/pages/Container/MonitorManage/index.js @@ -0,0 +1,12 @@ + +import React from "react"; + +function MonitorManage(props) { + return ( + + props.children + + ); +} + +export default MonitorManage; diff --git a/src/utils/enterpriseInfo/adapter.js b/src/utils/enterpriseInfo/adapter.js index dcf6d40..50527f8 100644 --- a/src/utils/enterpriseInfo/adapter.js +++ b/src/utils/enterpriseInfo/adapter.js @@ -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 = {}) { diff --git a/src/utils/enterpriseInfo/http.js b/src/utils/enterpriseInfo/http.js index 661572b..70fa95e 100644 --- a/src/utils/enterpriseInfo/http.js +++ b/src/utils/enterpriseInfo/http.js @@ -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) => {