dev_1.0.1
tangjie 2026-07-14 10:09:08 +08:00
parent 0a0d207409
commit 9c8e386773
9 changed files with 1029 additions and 32 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
/prototype
/dist
/demo
/doc
/docs
/.trae/
npm-debug.log*

View File

@ -62,3 +62,17 @@ export const modifyFilingMaterial = declareRequest(
"qualFilingMaterialLoading",
"Post > @/safetyEval/qual-filing-material/modify",
);
// ─── 资质保持监控 ───
export const queryQualMonitorPage = declareRequest(
"qualReviewLoading",
"Get > /safetyEval/qual-monitor/page",
'qualMonitorList: [] | res.data || [] & qualMonitorTotal: 0 | res.total || 0',
);
export const queryQualMonitorCondition = declareRequest(
"qualReviewLoading",
"Get > /safetyEval/qual-monitor/condition-detail",
'qualMonitorCondition: {} | res.data || {}',
);

View File

@ -0,0 +1,136 @@
import React, { useEffect, useRef, useState } from 'react';
import { Modal, Input, message } from 'antd';
import './index.less';
const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markerRef = useRef(null);
const [loaded, setLoaded] = useState(false);
const [selectedPoint, setSelectedPoint] = useState(null);
// 动态加载百度地图脚本
useEffect(() => {
if (!visible) return;
if (window.BMap) {
// 如果已有 BMap等 DOM 渲染后再初始化
setTimeout(() => setLoaded(true), 100);
return;
}
const scriptId = 'baiduMapScript';
if (document.getElementById(scriptId)) return;
window.__onBMapLoaded = () => {
setLoaded(true);
};
const script = document.createElement('script');
script.id = scriptId;
script.src = `https://api.map.baidu.com/api?v=3.0&ak=6lkWDSnHTCcvUenZhovQPER9nkhFfT9A&callback=__onBMapLoaded`;
script.async = true;
document.body.appendChild(script);
}, [visible]);
// 初始化地图
useEffect(() => {
if (!loaded || !visible || !mapRef.current) return;
// 避免重复初始化
if (mapInstanceRef.current) return;
const map = new window.BMap.Map(mapRef.current);
// 默认以重庆市中心为中心
map.centerAndZoom(new window.BMap.Point(106.55, 29.57), 12);
map.enableScrollWheelZoom(true);
mapInstanceRef.current = map;
// 点击地图获取坐标
map.addEventListener('click', (e) => {
const point = e.point;
setSelectedPoint(point);
if (markerRef.current) {
map.removeOverlay(markerRef.current);
}
const marker = new window.BMap.Marker(point);
map.addOverlay(marker);
markerRef.current = marker;
});
// 搜索自动完成
const ac = new window.BMap.Autocomplete({
input: 'baiduSearchInput',
location: map,
});
ac.addEventListener('onconfirm', (e) => {
const value = e.item.value;
const local = new window.BMap.LocalSearch(map, {
onSearchComplete: (results) => {
if (results.getCurrentNumPois() > 0) {
const poi = results.getPoi(0);
const point = poi.point;
setSelectedPoint(point);
map.centerAndZoom(point, 15);
if (markerRef.current) {
map.removeOverlay(markerRef.current);
}
const marker = new window.BMap.Marker(point);
map.addOverlay(marker);
markerRef.current = marker;
}
},
});
local.search(
[value.province, value.city, value.district, value.street, value.business]
.filter(Boolean)
.join(''),
);
});
return () => {
mapInstanceRef.current = null;
};
}, [loaded, visible]);
// 弹窗关闭时重置
useEffect(() => {
if (!visible) {
setLoaded(false);
setSelectedPoint(null);
markerRef.current = null;
mapInstanceRef.current = null;
}
}, [visible]);
const handleConfirm = () => {
if (!selectedPoint) {
message.warning('请在地图上选择位置');
return;
}
onConfirm({
lng: selectedPoint.lng,
lat: selectedPoint.lat,
});
};
return (
<Modal
title="选择坐标"
open={visible}
onCancel={onCancel}
onOk={handleConfirm}
width={800}
okText="确认"
cancelText="取消"
destroyOnClose
>
<input
id="baiduSearchInput"
className="baidu-map-search"
placeholder="输入地名搜索"
/>
<div ref={mapRef} className="baidu-map-container" />
</Modal>
);
};
export default BaiduMapPicker;

View File

@ -0,0 +1,33 @@
.baidu-map-search {
width: 100%;
height: 36px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
box-sizing: border-box;
margin-bottom: 12px;
font-size: 14px;
transition: border-color 0.2s;
&:focus {
border-color: #4096ff;
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
}
&::placeholder {
color: #bfbfbf;
}
}
.baidu-map-container {
width: 100%;
height: 480px;
border-radius: 6px;
overflow: hidden;
}
/* 百度地图 Autocomplete 联想面板默认 z-index 过低,被 Modal 遮挡 */
.tangram-suggestion-main {
z-index: 10000 !important;
}

View File

@ -61,6 +61,14 @@ export const CHANGE_STATUS_MAP = {
approved: { label: "已审核", color: "success" },
};
/** 资质保持监控状态 */
export const QUAL_MONITOR_STATUS_MAP = {
normal: { label: "保持正常", color: "success" },
person_warning: { label: "人员比例预警", color: "warning" },
focus_warning: { label: "重点预警", color: "error" },
area_pending: { label: "属地待核验", color: "warning" },
};
/** 性别 */
export const GENDER_OPTIONS = [
{ label: "男", value: 1 },

View File

@ -10,9 +10,10 @@ import {
Row,
Select,
Space,
Flex
} from "antd";
import AttachmentUpload from "~/components/AttachmentUpload";
import BaiduMapPicker from "~/components/BaiduMapPicker";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
@ -44,6 +45,7 @@ function OrgInfoPage(props) {
const [editing, setEditing] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [detail, setDetail] = useState({});
const [mapPickerVisible, setMapPickerVisible] = useState(false);
/** 是否已存在机构数据(有 id 视为已入库,只能修改) */
const [hasExistingData, setHasExistingData] = useState(false);
@ -128,6 +130,10 @@ function OrgInfoPage(props) {
setSubmitting(false);
}
};
const handleMapConfirm = ({ lng, lat }) => {
form.setFieldsValue({ longitude: lng, latitude: lat });
setMapPickerVisible(false);
};
return (
<PageLayout
@ -160,7 +166,11 @@ function OrgInfoPage(props) {
label="生产经营单位名称"
rules={[{ required: true, message: "请输入生产经营单位名称" }]}
>
<Input placeholder="请输入生产经营单位名称" allowClear maxLength={200} />
<Input
placeholder="请输入生产经营单位名称"
allowClear
maxLength={200}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -169,7 +179,11 @@ function OrgInfoPage(props) {
label="统一社会信用代码"
rules={[creditCodeRule(true)]}
>
<Input placeholder="请输入统一社会信用代码" allowClear maxLength={18} />
<Input
placeholder="请输入统一社会信用代码"
allowClear
maxLength={18}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -180,7 +194,11 @@ function OrgInfoPage(props) {
{ required: true, message: "请输入安全生产监管行业类别" },
]}
>
<Input placeholder="请输入安全生产监管行业类别" allowClear maxLength={100} />
<Input
placeholder="请输入安全生产监管行业类别"
allowClear
maxLength={100}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -206,31 +224,62 @@ function OrgInfoPage(props) {
label="所属镇、街道"
rules={[{ required: true, message: "请输入所属镇街道" }]}
>
<Input placeholder="请输入所属镇街道" allowClear maxLength={100} />
<Input
placeholder="请输入所属镇街道"
allowClear
maxLength={100}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="villageCommunity" label="属村(社区)">
<Input placeholder="请输入属村(社区)" allowClear maxLength={100} />
<Input
placeholder="请输入属村(社区)"
allowClear
maxLength={100}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="longitude"
label="所在地坐标经度"
dependencies={["latitude"]}
rules={[
longitudeRule(false),
coordinatePairRule("latitude", "经度", "纬度"),
]}
>
<InputNumber
style={{ width: "100%" }}
min={-180}
max={180}
precision={6}
placeholder="请输入经度"
/>
<Form.Item label="所在地坐标经度">
<Flex>
<Form.Item
name="longitude"
noStyle
dependencies={["latitude"]}
rules={[
{
validator(_, value) {
const latitude = form.getFieldValue("latitude");
if (
value !== undefined &&
value !== null &&
value !== "" &&
(latitude === undefined ||
latitude === null ||
latitude === "")
) {
return Promise.reject(
new Error("填写经度后,纬度必填"),
);
}
return Promise.resolve();
},
},
]}
>
<InputNumber
style={{ width: "100%" }}
min={-180}
max={180}
precision={6}
placeholder="请输入经度"
allowClear
/>
</Form.Item>
<Button onClick={() => setMapPickerVisible(true)}>选择</Button>
</Flex>
</Form.Item>
</Col>
<Col span={12}>
@ -258,7 +307,11 @@ function OrgInfoPage(props) {
label="注册地址"
rules={[{ required: true, message: "请输入注册地址" }]}
>
<Input.TextArea rows={3} placeholder="请输入注册地址" maxLength={200} />
<Input.TextArea
rows={3}
placeholder="请输入注册地址"
maxLength={200}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -267,7 +320,11 @@ function OrgInfoPage(props) {
label="经营地址"
rules={[{ required: true, message: "请输入经营地址" }]}
>
<Input.TextArea rows={3} placeholder="请输入经营地址" maxLength={200} />
<Input.TextArea
rows={3}
placeholder="请输入经营地址"
maxLength={200}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -280,7 +337,11 @@ function OrgInfoPage(props) {
name="economyIndustryCode"
label="国民经济行业分类(GB/T4754-2017)"
>
<Input placeholder="请输入国民经济行业分类" allowClear maxLength={50} />
<Input
placeholder="请输入国民经济行业分类"
allowClear
maxLength={50}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -301,7 +362,11 @@ function OrgInfoPage(props) {
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入法定代表人联系电话" allowClear maxLength={11} />
<Input
placeholder="请输入法定代表人联系电话"
allowClear
maxLength={11}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -322,12 +387,20 @@ function OrgInfoPage(props) {
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入主要负责人联系电话" allowClear maxLength={11} />
<Input
placeholder="请输入主要负责人联系电话"
allowClear
maxLength={11}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="safetyDeptManager" label="安全管理部门负责人">
<Input placeholder="请输入安全管理部门负责人" allowClear maxLength={50} />
<Input
placeholder="请输入安全管理部门负责人"
allowClear
maxLength={50}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -356,7 +429,11 @@ function OrgInfoPage(props) {
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入主管安全副总联系电话" allowClear maxLength={11} />
<Input
placeholder="请输入主管安全副总联系电话"
allowClear
maxLength={11}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -369,7 +446,11 @@ function OrgInfoPage(props) {
</Col>
<Col span={12}>
<Form.Item name="businessStatusName" label="企业经营状态">
<Input placeholder="请输入企业经营状态" allowClear maxLength={50} />
<Input
placeholder="请输入企业经营状态"
allowClear
maxLength={50}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -381,7 +462,11 @@ function OrgInfoPage(props) {
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入信息公开网址" allowClear maxLength={200} />
<Input
placeholder="请输入信息公开网址"
allowClear
maxLength={200}
/>
</Form.Item>
</Col>
<Col span={12}>
@ -393,6 +478,7 @@ function OrgInfoPage(props) {
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={2}
placeholder="请输入工作场所建筑面积"
/>
@ -407,6 +493,7 @@ function OrgInfoPage(props) {
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={2}
placeholder="请输入档案室面积"
/>
@ -421,6 +508,7 @@ function OrgInfoPage(props) {
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={0}
placeholder="请输入专职安全评价师数量"
/>
@ -435,6 +523,7 @@ function OrgInfoPage(props) {
<InputNumber
style={{ width: "100%" }}
min={0}
max={99999}
precision={0}
placeholder="请输入注册安全工程师数量"
/>
@ -503,10 +592,21 @@ function OrgInfoPage(props) {
<Form.Item name="filingRecordStatusName" noStyle />
</Col>
<Col span={12}>
<AttachmentUpload name="attachmentUrls" label="上传附件" />
<AttachmentUpload
name="attachmentUrls"
label="上传附件"
extra={"最多上传10个PDF、DOC、DOCX格式文件"}
maxCount={10}
accept=".pdf,.doc,.docx"
/>
</Col>
</Row>
</Form>
<BaiduMapPicker
visible={mapPickerVisible}
onCancel={() => setMapPickerVisible(false)}
onConfirm={handleMapConfirm}
/>
{editing && (
<div style={{ marginTop: 24, textAlign: "center" }}>
<Space>

View File

@ -106,6 +106,11 @@ const menuItems = [
label: "备案变更管理",
icon: <SwapOutlined />,
},
{
key: "/safetyEval/container/QualificationReview/QualMonitor",
label: "资质保持监控",
icon: <BarChartOutlined />,
},
],
},
]

View File

@ -0,0 +1,532 @@
import React, { useState, useEffect } from "react";
import { Button, Form, Select, Table, Tag, Modal } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
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 { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_REVIEW } from "~/enumerate/namespace";
import { QUAL_MONITOR_STATUS_MAP } from "~/enumerate/constant";
import "./index.less";
const { router } = tools;
/** 资质条件检查项模板 */
const CONDITION_ITEMS = [
{ key: "legalPerson", label: "独立法人资格" },
{ key: "fixedAsset", label: "固定资产" },
{ key: "workplace", label: "工作场所" },
{ key: "equipment", label: "设施设备软件" },
{ key: "staffCount", label: "专职技术人员数量" },
{ key: "staffRatio", label: "人员比例" },
{ key: "leader", label: "负责人配备" },
{ key: "system", label: "制度与过程控制体系" },
{ key: "website", label: "承诺书/网站/报告查询" },
{ key: "credit", label: "三年重大违法失信记录" },
{ key: "area", label: "属地满足情况" },
];
/**
* Mock 数据 后端接口就绪后删除此数据及下方 MOCK_INSTITUTIONS 引用
* 改用 props.qualReview.qualMonitorList 即可
*/
const MOCK_INSTITUTIONS = [
{
id: 1,
orgName: "重庆安评技术研究院有限公司",
creditCode: "91500112MA5UQ001",
registerArea: "重庆市渝北区",
bizScopes: ["石油加工业,化学原料、化学品及医药制造业", "金属冶炼"],
bizScopeCount: 2,
minStaffRequire: 30,
status: "normal",
fixedAsset: "1260 万元",
workplace: "1280 ㎡",
archiveRoom: "160 ㎡",
staffCount: 36,
midSafeEngineer: 12,
midSafeRatio: "33%",
seniorTitle: 13,
seniorRatio: "36%",
totalRatio: "69%",
techLeader: "2/2",
processLeader: "1/1",
systemStatus: "制度体系已备案",
promiseStatus: "承诺书已上传",
websiteStatus: "网站可查询",
creditStatus: "近三年无重大违法失信记录",
sourceFields: ["机构备案.固定资产", "场所核验.面积", "人员库.专职人员", "网站备案.报告查询"],
assetOk: true,
workplaceOk: true,
archiveOk: true,
staffOk: true,
conditionDetails: {
legalPerson: { require: "具有独立法人资格", value: "独立法人", result: "通过", source: "机构备案.法人类型、营业执照附件" },
fixedAsset: { require: "不少于 800 万元", value: "1260 万元", result: "通过", source: "财务材料.固定资产净值、审计报告" },
workplace: { require: "建筑面积不少于 1000 平方米,档案室不少于 100 平方米", value: "工作场所 1280 平方米,档案室 160 平方米", result: "通过", source: "场所核验.建筑面积、档案室核验.面积" },
equipment: { require: "具有与业务相适应的技术支撑条件", value: "设备台账、软件授权、检测工具齐全", result: "通过", source: "设备台账.设备清单、软件授权.有效期" },
staffCount: { require: "单一业务范围不少于 25 人", value: "2 个业务范围36 人", result: "通过", source: "人员库.专职人员、业务范围.数量" },
staffRatio: { require: "注安≥30%高工≥30%合计≥60%", value: "注安 33%,高工 36%,合计 69%", result: "通过", source: "人员库.注册安全工程师、人员库.职称等级" },
leader: { require: "每个业务范围配备专职技术负责人", value: "技术负责人 2/2过程控制负责人 1/1", result: "通过", source: "负责人备案.技术负责人、负责人备案.过程控制负责人" },
system: { require: "具有健全内部管理制度和安全评价过程控制体系", value: "制度文件已备案,过程控制节点可追溯", result: "通过", source: "制度文件.版本、过程控制.节点记录" },
website: { require: "法定代表人承诺书已出具;网站正常运行", value: "承诺书已上传;网站可查询机构信息和报告", result: "通过", source: "承诺书.上传状态、网站备案.报告查询状态" },
credit: { require: "三年内无重大违法失信记录", value: "未命中重大违法失信记录", result: "通过", source: "信用核查.重大违法失信记录" },
area: { require: "注册地所在行政区域范围内满足资质条件", value: "注册地与资质条件材料一致", result: "通过", source: "异地备案.注册地、本市社保.缴纳人数" },
},
},
{
id: 2,
orgName: "重庆恒安安全评价有限公司",
creditCode: "91500103MA60H002",
registerArea: "重庆市江北区",
bizScopes: ["石油加工业,化学原料、化学品及医药制造业"],
bizScopeCount: 1,
minStaffRequire: 25,
status: "person_warning",
fixedAsset: "920 万元",
workplace: "1040 ㎡",
archiveRoom: "110 ㎡",
staffCount: 27,
midSafeEngineer: 9,
midSafeRatio: "33%",
seniorTitle: 7,
seniorRatio: "26%",
totalRatio: "59%",
techLeader: "1/1",
processLeader: "1/1",
systemStatus: "制度体系已备案",
promiseStatus: "承诺书已上传",
websiteStatus: "网站可查询",
creditStatus: "近三年无重大违法失信记录",
sourceFields: ["人员库.职称等级", "人员库.注册安全工程师", "社保比对.缴纳单位", "业务范围.数量"],
assetOk: true,
workplaceOk: true,
archiveOk: true,
staffOk: true,
conditionDetails: {
legalPerson: { require: "具有独立法人资格", value: "独立法人", result: "通过", source: "机构备案.法人类型" },
fixedAsset: { require: "不少于 800 万元", value: "920 万元", result: "通过", source: "财务材料.固定资产净值" },
workplace: { require: "建筑面积不少于 1000 平方米,档案室不少于 100 平方米", value: "工作场所 1040 平方米,档案室 110 平方米", result: "通过", source: "场所核验.建筑面积、档案室核验.面积" },
equipment: { require: "具有与业务相适应的技术支撑条件", value: "设备台账、软件授权齐全", result: "通过", source: "设备台账.设备清单" },
staffCount: { require: "单一业务范围不少于 25 人", value: "1 个业务范围27 人", result: "通过", source: "人员库.专职人员" },
staffRatio: { require: "注安≥30%高工≥30%合计≥60%", value: "注安 33%,高工 26%,合计 59%", result: "预警", source: "人员库.职称等级" },
leader: { require: "每个业务范围配备专职技术负责人", value: "技术负责人 1/1过程控制负责人 1/1", result: "通过", source: "负责人备案" },
system: { require: "具有健全内部管理制度和安全评价过程控制体系", value: "制度文件已备案", result: "通过", source: "制度文件" },
website: { require: "法定代表人承诺书已出具;网站正常运行", value: "承诺书已上传;网站可查询", result: "通过", source: "网站备案" },
credit: { require: "三年内无重大违法失信记录", value: "未命中", result: "通过", source: "信用核查" },
area: { require: "注册地所在行政区域范围内满足资质条件", value: "注册地与资质条件材料一致", result: "通过", source: "本市社保.缴纳人数" },
},
},
{
id: 3,
orgName: "重庆渝安风险评估中心",
creditCode: "91500108MA61Y003",
registerArea: "重庆市南岸区",
bizScopes: ["金属、非金属矿及其他矿采选业"],
bizScopeCount: 1,
minStaffRequire: 25,
status: "focus_warning",
fixedAsset: "760 万元",
workplace: "980 ㎡",
archiveRoom: "90 ㎡",
staffCount: 24,
midSafeEngineer: 7,
midSafeRatio: "29%",
seniorTitle: 8,
seniorRatio: "33%",
totalRatio: "62%",
techLeader: "1/1",
processLeader: "1/1",
systemStatus: "制度体系已备案",
promiseStatus: "承诺书已上传",
websiteStatus: "网站报告查询接口异常",
creditStatus: "近三年无重大违法失信记录",
sourceFields: ["财务材料.固定资产", "场所核验.建筑面积", "档案室核验.面积", "人员库.专职人数", "网站监测.查询状态"],
assetOk: false,
workplaceOk: false,
archiveOk: false,
staffOk: false,
conditionDetails: {
legalPerson: { require: "具有独立法人资格", value: "独立法人", result: "通过", source: "机构备案.法人类型" },
fixedAsset: { require: "不少于 800 万元", value: "760 万元", result: "不通过", source: "财务材料.固定资产净值" },
workplace: { require: "建筑面积不少于 1000 平方米,档案室不少于 100 平方米", value: "工作场所 980 平方米,档案室 90 平方米", result: "不通过", source: "场所核验.建筑面积" },
equipment: { require: "具有与业务相适应的技术支撑条件", value: "设备台账齐全,报告查询接口异常", result: "部分异常", source: "网站监测.查询状态" },
staffCount: { require: "单一业务范围不少于 25 人", value: "1 个业务范围24 人", result: "不通过", source: "人员库.专职人数" },
staffRatio: { require: "注安≥30%高工≥30%合计≥60%", value: "注安 29%,高工 33%,合计 62%", result: "预警", source: "人员库.注册安全工程师" },
leader: { require: "每个业务范围配备专职技术负责人", value: "技术负责人 1/1过程控制负责人 1/1", result: "通过", source: "负责人备案" },
system: { require: "具有健全内部管理制度和安全评价过程控制体系", value: "制度文件已备案", result: "通过", source: "制度文件" },
website: { require: "法定代表人承诺书已出具;网站正常运行", value: "承诺书已上传;网站报告查询接口异常", result: "预警", source: "网站备案.报告查询状态" },
credit: { require: "三年内无重大违法失信记录", value: "未命中", result: "通过", source: "信用核查" },
area: { require: "注册地所在行政区域范围内满足资质条件", value: "注册地与资质条件材料一致", result: "通过", source: "本市社保" },
},
},
{
id: 4,
orgName: "北京中安评价中心重庆分公司",
creditCode: "91110108MA62B004",
registerArea: "北京市,重庆备案场所:两江新区",
bizScopes: ["陆上油气管道运输业"],
bizScopeCount: 1,
minStaffRequire: 25,
status: "area_pending",
fixedAsset: "1080 万元",
workplace: "1120 ㎡",
archiveRoom: "120 ㎡",
staffCount: 31,
midSafeEngineer: 10,
midSafeRatio: "32%",
seniorTitle: 9,
seniorRatio: "29%",
totalRatio: "61%",
techLeader: "1/1",
processLeader: "1/1",
systemStatus: "制度体系已备案",
promiseStatus: "承诺书已上传",
websiteStatus: "网站可查询",
creditStatus: "近三年无重大违法失信记录",
sourceFields: ["异地备案.注册地", "本市社保.缴纳人数", "场所核验.租赁合同", "业务范围.油气管道"],
assetOk: true,
workplaceOk: true,
archiveOk: true,
staffOk: true,
conditionDetails: {
legalPerson: { require: "具有独立法人资格", value: "总机构独立法人,重庆为分公司备案", result: "需核验", source: "机构备案.法人类型" },
fixedAsset: { require: "不少于 800 万元", value: "1080 万元", result: "通过", source: "财务材料.固定资产净值" },
workplace: { require: "建筑面积不少于 1000 平方米,档案室不少于 100 平方米", value: "重庆场所 1120 平方米,档案室 120 平方米", result: "通过", source: "场所核验.租赁合同" },
equipment: { require: "具有与业务相适应的技术支撑条件", value: "设备台账齐全", result: "通过", source: "设备台账" },
staffCount: { require: "单一业务范围不少于 25 人", value: "备案 31 人,本市社保 21 人", result: "不通过", source: "社保比对.缴纳单位" },
staffRatio: { require: "注安≥30%高工≥30%合计≥60%", value: "本市专职人员比例待核验", result: "需核验", source: "人员库.本市社保" },
leader: { require: "每个业务范围配备专职技术负责人", value: "技术负责人 1/1过程控制负责人 1/1", result: "通过", source: "负责人备案" },
system: { require: "具有健全内部管理制度和安全评价过程控制体系", value: "制度文件已备案", result: "通过", source: "制度文件" },
website: { require: "法定代表人承诺书已出具;网站正常运行", value: "承诺书已上传;网站可查询", result: "通过", source: "网站备案" },
credit: { require: "三年内无重大违法失信记录", value: "未命中", result: "通过", source: "信用核查" },
area: { require: "注册地所在行政区域范围内满足资质条件", value: "重庆备案场所达标,本市社保专职人员不足", result: "需核验", source: "异地备案.注册地" },
},
},
];
const STATUS_OPTIONS = [
{ value: "", label: "全部" },
{ value: "normal", label: "保持正常" },
{ value: "person_warning", label: "人员比例预警" },
{ value: "focus_warning", label: "重点预警" },
{ value: "area_pending", label: "属地待核验" },
];
const SCOPE_OPTIONS = [
{ value: "", label: "全部" },
{ value: "化工", label: "石油加工业,化学原料、化学品及医药制造业" },
{ value: "金属冶炼", label: "金属冶炼" },
{ value: "矿采选", label: "金属、非金属矿及其他矿采选业" },
{ value: "油气管道", label: "陆上油气管道运输业" },
];
/** 统计卡片数据 */
function computeStats(list) {
return {
total: list.length,
normal: list.filter((i) => i.status === "normal").length,
warning: list.filter((i) => i.status === "person_warning" || i.status === "area_pending").length,
focus: list.filter((i) => i.status === "focus_warning").length,
};
}
const QualMonitor = (props) => {
const [form] = Form.useForm();
const [detailVisible, setDetailVisible] = useState(false);
const [detailRecord, setDetailRecord] = useState(null);
// 待后端接口就绪后data 改为使用 props.qualReview.qualMonitorList
// 并移除 MOCK_INSTITUTIONS 引用
const [data, setData] = useState(MOCK_INSTITUTIONS);
const { qualReview, queryQualMonitorPage, queryQualMonitorCondition } = props;
const { qualMonitorList, qualMonitorTotal, qualReviewLoading } = qualReview || {};
const stats = computeStats(data);
/** 搜索 — 当前使用 mock 本地过滤;接接口后改为调用 queryQualMonitorPage */
const onFinish = (values) => {
const { keyword, scope, status } = values;
let filtered = MOCK_INSTITUTIONS;
if (keyword) {
filtered = filtered.filter(
(i) => i.orgName.includes(keyword) || i.creditCode.includes(keyword),
);
}
if (scope) {
filtered = filtered.filter((i) =>
i.bizScopes.some((s) => s.includes(scope)),
);
}
if (status) {
filtered = filtered.filter((i) => i.status === status);
}
router.query = { ...router.query, ...values, current: 1, size: 10 };
setData(filtered);
};
/** 接接口后改为 queryQualMonitorPage 调用 */
const handleSearch = () => {
// queryQualMonitorPage(router.query);
};
const handleReset = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
handleSearch();
};
useEffect(() => {
form.setFieldsValue(router.query);
// 接接口后打开下行注释
// handleSearch();
}, []);
const openDetail = (record) => {
setDetailRecord(record);
setDetailVisible(true);
};
const resultTag = (result) => {
if (result === "通过") return <Tag color="success">通过</Tag>;
if (result === "预警" || result === "需核验") return <Tag color="warning">{result}</Tag>;
if (result === "不通过" || result === "部分异常") return <Tag color="error">{result}</Tag>;
return <Tag>{result}</Tag>;
};
const columns = [
{
title: "机构信息",
dataIndex: "orgName",
width: 240,
fixed: "left",
render: (name, record) => (
<div className="org-info">
<div className="org-name">{name}</div>
<div className="org-meta">
统一社会信用代码{record.creditCode} | 注册地{record.registerArea}
</div>
</div>
),
},
{
title: "具备资质/业务范围",
dataIndex: "bizScopes",
width: 260,
render: (scopes, record) => (
<div className="biz-scope-wrap">
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{scopes.map((s, i) => (
<Tag key={i} color="blue">{s}</Tag>
))}
</div>
<div className="biz-scope-hint">
业务范围数{record.bizScopeCount}最低专职人员要求{record.minStaffRequire}
</div>
</div>
),
},
{
title: "状态",
dataIndex: "status",
width: 110,
render: (status) => {
const config = QUAL_MONITOR_STATUS_MAP[status];
return config ? <Tag color={config.color}>{config.label}</Tag> : "--";
},
},
{
title: "固定资产/场所",
dataIndex: "fixedAsset",
width: 260,
render: (_, record) => (
<div>
<div>固定资产 {record.fixedAsset}工作场所 {record.workplace}档案室 {record.archiveRoom}</div>
<div className={`place-status ${record.assetOk && record.workplaceOk ? "place-ok" : "place-bad"}`}>
{record.assetOk && record.workplaceOk ? "均满足资质条件" : "三项基础条件低于要求"}
</div>
</div>
),
},
{
title: "人员与负责人",
dataIndex: "staffCount",
width: 300,
render: (_, record) => (
<div>
专职技术人员 {record.staffCount} 中级及以上注册安全工程师 {record.midSafeEngineer} /{record.midSafeRatio}
高级职称 {record.seniorTitle} /{record.seniorRatio}合计 {record.totalRatio}
技术负责人 {record.techLeader}过程控制负责人 {record.processLeader}
</div>
),
},
{
title: "制度/网站/信用",
dataIndex: "systemStatus",
width: 220,
render: (_, record) => (
<div>
{record.systemStatus}{record.promiseStatus}{record.websiteStatus}{record.creditStatus}
</div>
),
},
{
title: "来源字段",
dataIndex: "sourceFields",
width: 200,
render: (fields) => (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{fields.map((f, i) => (
<span key={i} className="source-pill">{f}</span>
))}
</div>
),
},
{
title: "操作",
width: 120,
fixed: "right",
render: (_, record) => (
<TableAction>
<Button type="primary" size="small" onClick={() => openDetail(record)}>
条件详情
</Button>
</TableAction>
),
},
];
return (
<PageLayout title="资质保持监控">
<div className="qual-monitor">
{/* 统计卡片 */}
<div className="stat-grid">
{[
{ label: "备案机构总数", value: stats.total, hint: "来源:已备案机构管理.机构状态", color: "#1e293b", iconBg: "#ede9fe", iconColor: "#7c3aed", iconText: "机" },
{ label: "条件保持正常", value: stats.normal, hint: "十项资质条件均通过", color: "#059669", iconBg: "#d1fae5", iconColor: "#059669", iconText: "正" },
{ label: "条件预警机构", value: stats.warning, hint: "人员比例、属地材料待核验", color: "#d97706", iconBg: "#fef3c7", iconColor: "#d97706", iconText: "预" },
].map((card, idx) => (
<div key={idx} className="stat-card">
<div className="stat-info">
<div className="stat-label">{card.label}</div>
<div className="stat-value" style={{ color: card.color }}>{card.value}</div>
<div className="stat-hint">{card.hint}</div>
</div>
<div className="stat-icon" style={{ background: card.iconBg, color: card.iconColor }}>
{card.iconText}
</div>
</div>
))}
</div>
{/* 资质条件来源说明 */}
<div className="source-note">
<strong>资质条件来源</strong>
依据安全评价机构资质条件将机构备案信息拆为固定资产不少于 800 万元工作场所不少于 1000 平方米档案室不少于 100 平方米
专职技术人员数量及比例技术负责人/过程控制负责人内部制度法定代表人承诺公开网站三年重大违法失信记录注册地属地满足情况等字段
</div>
{/* 搜索表单 */}
<SearchForm
style={{ marginBottom: 12 }}
form={form}
loading={qualReviewLoading}
formLine={[
<Form.Item key="keyword" name="keyword">
<ControlWrapper.Input
label="机构名称/信用代码"
placeholder="输入机构名称或统一社会信用代码"
allowClear
/>
</Form.Item>,
<Form.Item key="scope" name="scope">
<ControlWrapper.Select
label="业务范围"
placeholder="请选择"
allowClear
style={{ width: "100%" }}
options={SCOPE_OPTIONS.map((o) => ({ label: o.label, value: o.value }))}
/>
</Form.Item>,
<Form.Item key="status" name="status">
<ControlWrapper.Select
label="资质状态"
placeholder="请选择"
allowClear
style={{ width: "100%" }}
options={STATUS_OPTIONS.map((o) => ({ label: o.label, value: o.value }))}
/>
</Form.Item>,
]}
onReset={handleReset}
onFinish={onFinish}
/>
{/* 数据表格 */}
<Table
rowKey="id"
columns={columns}
dataSource={data}
scroll={{ x: 1600, y: props.scrollY }}
loading={qualReviewLoading}
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 };
},
}}
/>
{/* 条件详情弹窗 */}
<Modal
title={detailRecord ? `资质保持条件详情 - ${detailRecord.orgName}` : ""}
open={detailVisible}
onCancel={() => setDetailVisible(false)}
width={960}
footer={
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<Button onClick={() => setDetailVisible(false)}>关闭</Button>
</div>
}
>
{detailRecord && (
<div>
<div className="check-note">
<strong>机构核查口径</strong>线
每项展示监管字段当前值结论和来源
</div>
<Table
rowKey="key"
size="small"
pagination={false}
columns={[
{ title: "检查项", dataIndex: "label", width: 120 },
{ title: "资质要求", dataIndex: "require", width: 280 },
{ title: "当前值", dataIndex: "value", width: 260 },
{ title: "结论", dataIndex: "result", width: 80, render: resultTag },
{ title: "来源字段", dataIndex: "source", render: (text) => <span className="cell-source">{text}</span> },
]}
dataSource={CONDITION_ITEMS.map((item) => {
const detail = detailRecord.conditionDetails[item.key];
return detail ? { key: item.key, label: item.label, ...detail } : null;
}).filter(Boolean)}
/>
</div>
)}
</Modal>
</div>
</PageLayout>
);
};
export default Connect([NS_QUAL_REVIEW], true)(AntdTableFuncControl(QualMonitor));

View File

@ -0,0 +1,168 @@
.qual-monitor {
// 页面描述条
.page-desc {
font-size: 0.82rem;
color: #64748b;
margin-bottom: 16px;
padding: 8px 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
// 统计卡片网格
.stat-grid {
display: grid;
grid-template-columns: repeat(3, 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-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;
strong {
color: #1e293b;
}
}
// 机构信息
.org-info {
.org-name {
font-weight: 500;
}
.org-meta {
font-size: 0.72rem;
color: #64748b;
}
}
// 业务范围标签
.biz-scope-wrap {
display: flex;
flex-wrap: wrap;
gap: 4px;
.biz-scope-hint {
font-size: 0.72rem;
color: #64748b;
margin-top: 4px;
}
}
// 场所状态
.place-status {
font-size: 0.72rem;
}
.place-ok {
color: #059669;
}
.place-bad {
color: #dc2626;
}
// 来源字段药丸
.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;
}
// 核查口径说明
.check-note {
margin-bottom: 12px;
font-size: 0.86rem;
color: #64748b;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
}
// 条件详情表格
.detail-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
th {
padding: 8px 12px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
background: #f8fafc;
}
td {
padding: 8px 12px;
border-bottom: 1px solid #f1f5f9;
}
.cell-source {
font-size: 0.78rem;
}
.cell-label {
font-weight: 500;
}
}
}