Merge branch 'dev' of http://47.92.113.182:3000/cq_anquan/safety-eval-service-frontend into dev
commit
71101bc50a
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,7 @@
|
|||
"antd": "^5.29.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^6.1.0",
|
||||
"history": "^4.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
|||
6059
pnpm-lock.yaml
6059
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
|||
allowBuilds:
|
||||
es5-ext: set this to true or false
|
||||
zy-react-library: set this to true or false
|
||||
hoist: true
|
||||
nodeLinker: hoisted
|
||||
onlyBuiltDependencies:
|
||||
- es5-ext
|
||||
- zy-react-library
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||
|
||||
/** 监督检查告知 — 监管端 / 机构端 */
|
||||
|
||||
/* ───────── 监管端 ───────── */
|
||||
|
||||
export const regulatorInspNoticeSave = declareRequest(
|
||||
"regulatorInspNoticeSaveLoading",
|
||||
"Post > @/safetyEval/regulator/insp-notice/save",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeIssue = declareRequest(
|
||||
"regulatorInspNoticeIssueLoading",
|
||||
"Post > @/safetyEval/regulator/insp-notice/issue",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticePage = declareRequest(
|
||||
"regulatorInspNoticePageLoading",
|
||||
"Get > /safetyEval/regulator/insp-notice/page",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeGet = declareRequest(
|
||||
"regulatorInspNoticeGetLoading",
|
||||
"Get > /safetyEval/regulator/insp-notice/get",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeClose = declareRequest(
|
||||
"regulatorInspNoticeCloseLoading",
|
||||
"Post > @/safetyEval/regulator/insp-notice/close",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeRemark = declareRequest(
|
||||
"regulatorInspNoticeRemarkLoading",
|
||||
"Post > @/safetyEval/regulator/insp-notice/remark",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeMaterials = declareRequest(
|
||||
"regulatorInspNoticeMaterialsLoading",
|
||||
"Get > /safetyEval/regulator/insp-notice/materials",
|
||||
);
|
||||
|
||||
export const regulatorInspNoticeUrge = declareRequest(
|
||||
"regulatorInspNoticeUrgeLoading",
|
||||
"Post > @/safetyEval/regulator/insp-notice/urge",
|
||||
);
|
||||
|
||||
/* ───────── 机构端 ───────── */
|
||||
|
||||
export const institutionInspNoticePage = declareRequest(
|
||||
"institutionInspNoticePageLoading",
|
||||
"Get > /safetyEval/institution/insp-notice/page",
|
||||
);
|
||||
|
||||
export const institutionInspNoticeGet = declareRequest(
|
||||
"institutionInspNoticeGetLoading",
|
||||
"Get > /safetyEval/institution/insp-notice/get",
|
||||
);
|
||||
|
||||
export const institutionInspNoticeConfirm = declareRequest(
|
||||
"institutionInspNoticeConfirmLoading",
|
||||
"Post > @/safetyEval/institution/insp-notice/confirm",
|
||||
);
|
||||
|
||||
export const institutionInspNoticeMaterialUpload = declareRequest(
|
||||
"institutionInspNoticeMaterialUploadLoading",
|
||||
"Post > @/safetyEval/institution/insp-notice/material/upload",
|
||||
);
|
||||
|
||||
export const institutionInspNoticeFeedback = declareRequest(
|
||||
"institutionInspNoticeFeedbackLoading",
|
||||
"Post > @/safetyEval/institution/insp-notice/feedback",
|
||||
);
|
||||
|
||||
export const institutionInspNoticeMaterials = declareRequest(
|
||||
"institutionInspNoticeMaterialsLoading",
|
||||
"Get > /safetyEval/institution/insp-notice/materials",
|
||||
);
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 监管端 - 安全评价画像 API(当前走 mock,对接后端时切换 declareRequest)
|
||||
*
|
||||
* 预留路径(待 OpenAPI 定稿后启用):
|
||||
* - GET /safetyEval/regulator/eval-portrait/page
|
||||
* - GET /safetyEval/regulator/eval-portrait/get
|
||||
* - GET /safetyEval/regulator/eval-portrait/rules
|
||||
* - POST /safetyEval/regulator/eval-portrait/export
|
||||
*/
|
||||
|
||||
import {
|
||||
queryEvalPortraitPage as mockPage,
|
||||
getEvalPortraitDetail as mockDetail,
|
||||
getEvalPortraitRules as mockRules,
|
||||
exportEvalPortrait as mockExport,
|
||||
} from "~/mock/regulator/evalPortrait";
|
||||
|
||||
// import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||
// export const evalPortraitPage = declareRequest(
|
||||
// "evalPortraitPageLoading",
|
||||
// "Get > /safetyEval/regulator/eval-portrait/page",
|
||||
// );
|
||||
// export const evalPortraitDetail = declareRequest(
|
||||
// "evalPortraitDetailLoading",
|
||||
// "Get > /safetyEval/regulator/eval-portrait/get",
|
||||
// );
|
||||
// export const evalPortraitRules = declareRequest(
|
||||
// "evalPortraitRulesLoading",
|
||||
// "Get > /safetyEval/regulator/eval-portrait/rules",
|
||||
// );
|
||||
// export const evalPortraitExport = declareRequest(
|
||||
// "evalPortraitExportLoading",
|
||||
// "Post > @/safetyEval/regulator/eval-portrait/export",
|
||||
// );
|
||||
|
||||
/** 分页列表 */
|
||||
export const evalPortraitPage = (params) => mockPage(params);
|
||||
|
||||
/** 画像详情 */
|
||||
export const evalPortraitDetail = (params) => mockDetail(params);
|
||||
|
||||
/** 指标说明 */
|
||||
export const evalPortraitRules = () => mockRules();
|
||||
|
||||
/** 导出画像 */
|
||||
export const evalPortraitExport = (params) => mockExport(params);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||
|
||||
/** 监管端-安评报告数据库 */
|
||||
|
||||
export const regulatorEvalReportSummary = declareRequest(
|
||||
"regulatorEvalReportSummaryLoading",
|
||||
"Get > /safetyEval/regulator/eval-report/summary",
|
||||
);
|
||||
|
||||
export const regulatorEvalReportPage = declareRequest(
|
||||
"regulatorEvalReportPageLoading",
|
||||
"Get > /safetyEval/regulator/eval-report/page",
|
||||
);
|
||||
|
||||
export const regulatorEvalReportDetail = declareRequest(
|
||||
"regulatorEvalReportDetailLoading",
|
||||
"Get > /safetyEval/regulator/eval-report/get",
|
||||
);
|
||||
|
||||
export const regulatorEvalReportAdd = declareRequest(
|
||||
"regulatorEvalReportAddLoading",
|
||||
"Post > @/safetyEval/regulator/eval-report/add",
|
||||
);
|
||||
|
||||
export const regulatorSpotcheckStart = declareRequest(
|
||||
"regulatorSpotcheckStartLoading",
|
||||
"Post > @/safetyEval/regulator/eval-report-spotcheck/start",
|
||||
);
|
||||
|
||||
export const regulatorSpotcheckReview = declareRequest(
|
||||
"regulatorSpotcheckReviewLoading",
|
||||
"Post > @/safetyEval/regulator/eval-report-spotcheck/review",
|
||||
);
|
||||
|
||||
export const regulatorSpotcheckList = declareRequest(
|
||||
"regulatorSpotcheckListLoading",
|
||||
"Get > /safetyEval/regulator/eval-report-spotcheck/list",
|
||||
);
|
||||
|
||||
export const regulatorSpotcheckDetail = declareRequest(
|
||||
"regulatorSpotcheckDetailLoading",
|
||||
"Get > /safetyEval/regulator/eval-report-spotcheck/get",
|
||||
);
|
||||
|
|
@ -24,4 +24,5 @@ export const NS_DRIVER = defineNamespace("driver");
|
|||
export const NS_RISK_CENTER = defineNamespace("riskCenter");
|
||||
export const NS_SAFETY_EVAL_BUSINESS = defineNamespace("safetyEvalBusiness");
|
||||
export const NS_REGULATOR_EVAL_REPORT = defineNamespace("regulatorEvalReport");
|
||||
export const NS_REGULATOR_EVAL_PORTRAIT = defineNamespace("regulatorEvalPortrait");
|
||||
export const NS_INSP_NOTICE = defineNamespace("inspNotice");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* 监管端 - 安全评价画像 mock 数据
|
||||
* 对齐原型 mod-institution-profile / openRegModal('institutionProfile'|'profileRules')
|
||||
*/
|
||||
|
||||
const LIST = [
|
||||
{
|
||||
id: "1",
|
||||
orgName: "重庆安评技术研究院有限公司",
|
||||
businessScope: "化工",
|
||||
projectCount: 36,
|
||||
onTimeRate: "96%",
|
||||
onTimeRateValue: 96,
|
||||
spotcheckPassRate: "94%",
|
||||
spotcheckPassRateValue: 94,
|
||||
openWarningCount: 0,
|
||||
supervisionResult: "无异常",
|
||||
score: 93,
|
||||
grade: "A级",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
orgName: "重庆恒安安全评价有限公司",
|
||||
businessScope: "矿山",
|
||||
projectCount: 28,
|
||||
onTimeRate: "82%",
|
||||
onTimeRateValue: 82,
|
||||
spotcheckPassRate: "86%",
|
||||
spotcheckPassRateValue: 86,
|
||||
openWarningCount: 2,
|
||||
supervisionResult: "1次整改",
|
||||
score: 81,
|
||||
grade: "B级",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
orgName: "重庆渝安风险评估中心",
|
||||
businessScope: "金属冶炼",
|
||||
projectCount: 19,
|
||||
onTimeRate: "74%",
|
||||
onTimeRateValue: 74,
|
||||
spotcheckPassRate: "79%",
|
||||
spotcheckPassRateValue: 79,
|
||||
openWarningCount: 3,
|
||||
supervisionResult: "2次整改",
|
||||
score: 72,
|
||||
grade: "C级",
|
||||
},
|
||||
];
|
||||
|
||||
/** 画像详情(按机构) */
|
||||
const DETAILS = {
|
||||
1: {
|
||||
id: "1",
|
||||
orgName: "重庆安评技术研究院有限公司",
|
||||
grade: "A级",
|
||||
score: 93,
|
||||
trend: "近三个季度综合得分 86 → 89 → 93,报告质量和预警闭环持续改善。",
|
||||
indicators: [
|
||||
{ name: "项目按期完成率", result: "92%", scoreText: "28/30", source: "安评项目管理.完成时间" },
|
||||
{ name: "报告抽查合格率", result: "90%", scoreText: "27/30", source: "报告抽查记录" },
|
||||
{ name: "风险预警闭环率", result: "100%", scoreText: "20/20", source: "风险预警中心" },
|
||||
{ name: "监督检查结果", result: "无重大问题", scoreText: "18/20", source: "监督检查告知" },
|
||||
],
|
||||
},
|
||||
2: {
|
||||
id: "2",
|
||||
orgName: "重庆恒安安全评价有限公司",
|
||||
grade: "B级",
|
||||
score: 81,
|
||||
trend: "近三个季度综合得分 78 → 79 → 81,履约时效仍有波动,需关注未闭环预警。",
|
||||
indicators: [
|
||||
{ name: "项目按期完成率", result: "82%", scoreText: "24/30", source: "安评项目管理.完成时间" },
|
||||
{ name: "报告抽查合格率", result: "86%", scoreText: "25/30", source: "报告抽查记录" },
|
||||
{ name: "风险预警闭环率", result: "85%", scoreText: "17/20", source: "风险预警中心" },
|
||||
{ name: "监督检查结果", result: "1次整改", scoreText: "15/20", source: "监督检查告知" },
|
||||
],
|
||||
},
|
||||
3: {
|
||||
id: "3",
|
||||
orgName: "重庆渝安风险评估中心",
|
||||
grade: "C级",
|
||||
score: 72,
|
||||
trend: "近三个季度综合得分 70 → 71 → 72,报告质量与监督检查整改完成情况需重点关注。",
|
||||
indicators: [
|
||||
{ name: "项目按期完成率", result: "74%", scoreText: "22/30", source: "安评项目管理.完成时间" },
|
||||
{ name: "报告抽查合格率", result: "79%", scoreText: "23/30", source: "报告抽查记录" },
|
||||
{ name: "风险预警闭环率", result: "70%", scoreText: "14/20", source: "风险预警中心" },
|
||||
{ name: "监督检查结果", result: "2次整改", scoreText: "13/20", source: "监督检查告知" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/** 指标说明(profileRules) */
|
||||
export const PROFILE_RULES = {
|
||||
title: "画像指标说明",
|
||||
sectionTitle: "指标口径",
|
||||
sectionDesc: "综合得分用于监管辅助研判,不替代行政认定。",
|
||||
rows: [
|
||||
{ name: "项目履约", weight: "30%", method: "按期完成率、节点超期次数" },
|
||||
{ name: "报告质量", weight: "30%", method: "抽查合格率、退回修改次数" },
|
||||
{ name: "风险处置", weight: "20%", method: "未闭环预警、逾期反馈" },
|
||||
{ name: "监督检查", weight: "20%", method: "检查结论、整改完成情况" },
|
||||
],
|
||||
};
|
||||
|
||||
const delay = (ms = 200) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* 分页查询机构画像列表
|
||||
* @param {{ orgName?: string, grade?: string, businessScope?: string, current?: number, size?: number }} params
|
||||
*/
|
||||
export async function queryEvalPortraitPage(params = {}) {
|
||||
await delay();
|
||||
const {
|
||||
orgName = "",
|
||||
grade = "",
|
||||
businessScope = "",
|
||||
current = 1,
|
||||
size = 10,
|
||||
} = params;
|
||||
let list = [...LIST];
|
||||
if (orgName) {
|
||||
const key = String(orgName).trim().toLowerCase();
|
||||
list = list.filter((item) => item.orgName.toLowerCase().includes(key));
|
||||
}
|
||||
if (grade) {
|
||||
list = list.filter((item) => item.grade === grade);
|
||||
}
|
||||
if (businessScope) {
|
||||
list = list.filter((item) => item.businessScope === businessScope);
|
||||
}
|
||||
const total = list.length;
|
||||
const start = (Number(current) - 1) * Number(size);
|
||||
const data = list.slice(start, start + Number(size));
|
||||
return { success: true, code: "0", data, total };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询机构画像详情
|
||||
* @param {{ id: string }} params
|
||||
*/
|
||||
export async function getEvalPortraitDetail(params = {}) {
|
||||
await delay();
|
||||
const id = String(params.id || "");
|
||||
const data = DETAILS[id] || null;
|
||||
if (!data) {
|
||||
return { success: false, code: "-1", errMessage: "画像不存在", data: null };
|
||||
}
|
||||
return { success: true, code: "0", data };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询画像指标说明
|
||||
*/
|
||||
export async function getEvalPortraitRules() {
|
||||
await delay(80);
|
||||
return { success: true, code: "0", data: PROFILE_RULES };
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出画像(前端 mock:仅返回成功提示,预留对接)
|
||||
* @param {{ id: string }} params
|
||||
*/
|
||||
export async function exportEvalPortrait(params = {}) {
|
||||
await delay(300);
|
||||
return {
|
||||
success: true,
|
||||
code: "0",
|
||||
message: "机构画像已生成",
|
||||
data: { id: params.id },
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,679 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Form,
|
||||
Table,
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
Select,
|
||||
ConfigProvider,
|
||||
message,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
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 { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { NS_INSP_NOTICE } from "~/enumerate/namespace";
|
||||
import dayjs from "dayjs";
|
||||
import "./index.less";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const API_HOST = window.process?.env?.app?.API_HOST || "";
|
||||
const UPLOAD_ACTION = `${API_HOST}/safetyEval/file/upload`;
|
||||
|
||||
/** 雪花 ID 须保持字符串,避免 Number() 精度丢失 */
|
||||
const toId = (id) => (id == null ? id : String(id));
|
||||
|
||||
const downloadBinaryPost = async (path, body, fallbackName) => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
token: sessionStorage.getItem("token") || "",
|
||||
};
|
||||
const orgInfoId = sessionStorage.getItem("orgInfoId");
|
||||
if (orgInfoId) {
|
||||
headers.orgInfoId = String(orgInfoId);
|
||||
}
|
||||
const res = await fetch(`${API_HOST}${path}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
throw new Error(json.errMessage || json.message || "导出失败");
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`请求失败(${res.status})`);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
let fileName = fallbackName;
|
||||
const disposition = res.headers.get("content-disposition") || "";
|
||||
const matched = disposition.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
|
||||
if (matched?.[1]) {
|
||||
try {
|
||||
fileName = decodeURIComponent(matched[1].replace(/"/g, ""));
|
||||
} catch {
|
||||
fileName = matched[1].replace(/"/g, "");
|
||||
}
|
||||
}
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.download = fileName;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
|
||||
const PROTO_THEME = {
|
||||
token: { borderRadius: 8, borderRadiusLG: 12, borderRadiusSM: 6 },
|
||||
};
|
||||
|
||||
/** 原型通知类型筛选项 */
|
||||
const MSG_TYPE_OPTIONS = [
|
||||
{ label: "整改通知", value: "整改通知" },
|
||||
{ label: "核查函", value: "核查函" },
|
||||
{ label: "补正通知", value: "补正通知" },
|
||||
{ label: "办结通知", value: "办结通知" },
|
||||
];
|
||||
|
||||
const TAB_STATUS = {
|
||||
all: undefined,
|
||||
unread: 1,
|
||||
processing: 2,
|
||||
feedback: 3,
|
||||
closed: 4,
|
||||
};
|
||||
|
||||
const TAB_ITEMS = [
|
||||
{ key: "all", label: "全部通知" },
|
||||
{ key: "unread", label: "待处理" },
|
||||
{ key: "processing", label: "处理中" },
|
||||
{ key: "feedback", label: "待监管复核" },
|
||||
{ key: "closed", label: "已办结" },
|
||||
];
|
||||
|
||||
const STATUS_FILTER_OPTIONS = [
|
||||
{ label: "待处理", value: 1 },
|
||||
{ label: "处理中", value: 2 },
|
||||
{ label: "待监管复核", value: 3 },
|
||||
{ label: "已办结", value: 4 },
|
||||
];
|
||||
|
||||
const PROCESS_TYPE_OPTIONS = [
|
||||
{ label: "提交整改材料", value: "提交整改材料" },
|
||||
{ label: "提交核查说明", value: "提交核查说明" },
|
||||
{ label: "补充证明材料", value: "补充证明材料" },
|
||||
{ label: "查看办结结果", value: "查看办结结果" },
|
||||
];
|
||||
|
||||
const statusTagClass = (code) => {
|
||||
if (code === 1) return "inst-tag-danger";
|
||||
if (code === 2) return "inst-tag-warning";
|
||||
if (code === 3) return "inst-tag-info";
|
||||
if (code === 4) return "inst-tag-success";
|
||||
return "";
|
||||
};
|
||||
|
||||
const statusDisplayName = (code, name) => {
|
||||
if (code === 1) return "待处理";
|
||||
if (code === 2) return "处理中";
|
||||
if (code === 3) return "待监管复核";
|
||||
if (code === 4) return "已办结";
|
||||
return name || "-";
|
||||
};
|
||||
|
||||
/** 列表「通知类型」展示:办结用办结通知,其余用检查类型名 */
|
||||
const noticeTypeLabel = (record) => {
|
||||
if (record.statusCode === 4) return "办结通知";
|
||||
return record.inspTypeName || "监督检查通知";
|
||||
};
|
||||
|
||||
const noticeTypeTagClass = (record) => {
|
||||
if (record.statusCode === 4) return "inst-tag-success";
|
||||
if (record.statusCode === 1) return "inst-tag-danger";
|
||||
if (record.statusCode === 3) return "inst-tag-info";
|
||||
return "inst-tag-warning";
|
||||
};
|
||||
|
||||
const formatDate = (val) => {
|
||||
if (!val) return "-";
|
||||
const d = dayjs(val);
|
||||
return d.isValid() ? d.format("YYYY-MM-DD") : val;
|
||||
};
|
||||
|
||||
const ModalSection = ({ title, desc, children }) => (
|
||||
<div className="inst-modal-section">
|
||||
<h4>{title}</h4>
|
||||
{desc ? <p>{desc}</p> : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const InstitutionInspectionNotice = (props) => {
|
||||
const [searchForm] = Form.useForm();
|
||||
const [processForm] = Form.useForm();
|
||||
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [metrics, setMetrics] = useState({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
feedback: 0,
|
||||
closed: 0,
|
||||
});
|
||||
|
||||
const [processOpen, setProcessOpen] = useState(false);
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [materials, setMaterials] = useState([]);
|
||||
const [uploadUrls, setUploadUrls] = useState([]);
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
const {
|
||||
institutionInspNoticePageLoading,
|
||||
institutionInspNoticeGetLoading,
|
||||
institutionInspNoticeConfirmLoading,
|
||||
institutionInspNoticeFeedbackLoading,
|
||||
institutionInspNoticeMaterialUploadLoading,
|
||||
} = props;
|
||||
|
||||
const buildQuery = (tab = activeTab) => {
|
||||
const values = searchForm.getFieldsValue();
|
||||
const tabStatus = TAB_STATUS[tab];
|
||||
const keywordParts = [values.keyword, values.msgType].filter(Boolean);
|
||||
return {
|
||||
keyword: keywordParts.length ? keywordParts.join(" ") : undefined,
|
||||
statusCode: values.statusCode ?? tabStatus,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
try {
|
||||
const [allRes, pendingRes, feedbackRes, closedRes] = await Promise.all([
|
||||
props.institutionInspNoticePage({ current: 1, size: 1 }),
|
||||
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 1 }),
|
||||
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 3 }),
|
||||
props.institutionInspNoticePage({ current: 1, size: 1, statusCode: 4 }),
|
||||
]);
|
||||
setMetrics({
|
||||
total: allRes?.total || 0,
|
||||
pending: pendingRes?.total || 0,
|
||||
feedback: feedbackRes?.total || 0,
|
||||
closed: closedRes?.total || 0,
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async (page = pageIndex, size = pageSize, tab = activeTab) => {
|
||||
const res = await props.institutionInspNoticePage({
|
||||
...buildQuery(tab),
|
||||
current: page,
|
||||
size,
|
||||
});
|
||||
if (res?.success !== false) {
|
||||
setDataSource(res?.data || []);
|
||||
setTotal(res?.total || 0);
|
||||
setPageIndex(page);
|
||||
setPageSize(size);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1, 10, "all");
|
||||
fetchMetrics();
|
||||
}, []);
|
||||
|
||||
const switchTab = (key) => {
|
||||
setActiveTab(key);
|
||||
searchForm.setFieldsValue({ statusCode: undefined });
|
||||
fetchData(1, pageSize, key);
|
||||
};
|
||||
|
||||
const openProcess = async (record) => {
|
||||
setProcessOpen(true);
|
||||
setDetail(null);
|
||||
setMaterials([]);
|
||||
setUploadUrls([]);
|
||||
processForm.resetFields();
|
||||
processForm.setFieldsValue({
|
||||
noticeNo: record.noticeNo,
|
||||
processType: "提交整改材料",
|
||||
processRemark: "请根据监管要求填写问题原因、处理措施和完成情况。",
|
||||
});
|
||||
const [detailRes, matRes] = await Promise.all([
|
||||
props.institutionInspNoticeGet({ id: String(record.id) }),
|
||||
props.institutionInspNoticeMaterials({ noticeId: toId(record.id) }),
|
||||
]);
|
||||
if (detailRes?.success !== false) {
|
||||
const data = detailRes?.data || null;
|
||||
setDetail(data);
|
||||
processForm.setFieldsValue({ noticeNo: data?.noticeNo || record.noticeNo });
|
||||
if (data?.statusCode === 4) {
|
||||
processForm.setFieldsValue({ processType: "查看办结结果" });
|
||||
}
|
||||
} else {
|
||||
message.error(detailRes?.errMessage || "获取详情失败");
|
||||
}
|
||||
if (matRes?.success !== false) {
|
||||
setMaterials(matRes?.data || []);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileUpload = (info) => {
|
||||
if (info.file.status === "done") {
|
||||
const responseData = info.file.response?.data || info.file.response || {};
|
||||
const fileUrl = responseData?.url || responseData?.fileUrl || "";
|
||||
if (!fileUrl) {
|
||||
message.error("上传成功但未返回文件地址");
|
||||
return;
|
||||
}
|
||||
setUploadUrls((prev) => [...prev, fileUrl]);
|
||||
} else if (info.file.status === "error") {
|
||||
message.error("上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!detail?.id) return;
|
||||
const values = await processForm.validateFields();
|
||||
|
||||
if (values.processType === "查看办结结果" || detail.statusCode === 4) {
|
||||
setProcessOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let current = detail;
|
||||
if (current.statusCode === 1) {
|
||||
const confirmRes = await props.institutionInspNoticeConfirm({
|
||||
id: toId(current.id),
|
||||
});
|
||||
if (confirmRes?.success === false) {
|
||||
message.error(confirmRes?.errMessage || "确认失败");
|
||||
return;
|
||||
}
|
||||
current = confirmRes?.data || { ...current, statusCode: 2 };
|
||||
}
|
||||
|
||||
const pendingMaterials = materials.filter(
|
||||
(m) => m.uploadStatusCode !== 2 && !m.fileUrl,
|
||||
);
|
||||
const urls = [...uploadUrls];
|
||||
for (let i = 0; i < Math.min(urls.length, pendingMaterials.length); i += 1) {
|
||||
const mat = pendingMaterials[i];
|
||||
const upRes = await props.institutionInspNoticeMaterialUpload({
|
||||
materialId: toId(mat.id),
|
||||
fileUrl: urls[i],
|
||||
});
|
||||
if (upRes?.success === false) {
|
||||
message.error(upRes?.errMessage || "材料上传失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.statusCode === 2 || detail.statusCode === 2 || detail.statusCode === 1) {
|
||||
const matRes = await props.institutionInspNoticeMaterials({
|
||||
noticeId: toId(detail.id),
|
||||
});
|
||||
const latest = matRes?.data || materials;
|
||||
const feedbackRes = await props.institutionInspNoticeFeedback({
|
||||
id: toId(detail.id),
|
||||
materials: latest
|
||||
.filter((m) => m.fileUrl || urls.length)
|
||||
.map((m, idx) => ({
|
||||
materialId: toId(m.id),
|
||||
fileUrl: m.fileUrl || urls[idx] || urls[0],
|
||||
}))
|
||||
.filter((m) => m.fileUrl),
|
||||
});
|
||||
if (feedbackRes?.success === false) {
|
||||
message.error(feedbackRes?.errMessage || "提交失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
message.success("处理材料已提交监管复核");
|
||||
setProcessOpen(false);
|
||||
fetchData(pageIndex, pageSize);
|
||||
fetchMetrics();
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setExportLoading(true);
|
||||
try {
|
||||
const query = buildQuery();
|
||||
const hasSelected = selectedRowKeys.length > 0;
|
||||
const body = {
|
||||
...query,
|
||||
scope: hasSelected ? "SELECTED" : "FILTERED",
|
||||
ids: hasSelected ? selectedRowKeys.map(toId) : undefined,
|
||||
};
|
||||
await downloadBinaryPost(
|
||||
"/safetyEval/institution/insp-notice/export-messages",
|
||||
body,
|
||||
"监管消息清单.csv",
|
||||
);
|
||||
message.success("监管消息清单已导出");
|
||||
setExportOpen(false);
|
||||
} catch (e) {
|
||||
message.error(e.message || "导出失败");
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "通知编号",
|
||||
dataIndex: "noticeNo",
|
||||
width: 140,
|
||||
render: (t) => <span className="inst-mono">{t || "-"}</span>,
|
||||
},
|
||||
{
|
||||
title: "通知类型",
|
||||
dataIndex: "inspTypeName",
|
||||
width: 110,
|
||||
render: (_, record) => (
|
||||
<span className={`inst-tag ${noticeTypeTagClass(record)}`}>
|
||||
{noticeTypeLabel(record)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "来源预警",
|
||||
dataIndex: "sourceRefNo",
|
||||
width: 140,
|
||||
render: (t) => t || "-",
|
||||
},
|
||||
{
|
||||
title: "来源字段",
|
||||
key: "sourceFields",
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<div className="inst-source-list">
|
||||
{record.sourceTypeName ? (
|
||||
<span className="inst-source-pill">{record.sourceTypeName}</span>
|
||||
) : (
|
||||
<span className="inst-source-pill">监督检查</span>
|
||||
)}
|
||||
{record.inspTypeName ? (
|
||||
<span className="inst-source-pill">{record.inspTypeName}</span>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "监管要求",
|
||||
dataIndex: "noticeContent",
|
||||
ellipsis: true,
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
title: "办理期限",
|
||||
dataIndex: "planInspTime",
|
||||
width: 120,
|
||||
render: formatDate,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "statusCode",
|
||||
width: 110,
|
||||
render: (code, record) => (
|
||||
<span className={`inst-tag ${statusTagClass(code)}`}>
|
||||
{statusDisplayName(code, record.statusName)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 90,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small" onClick={() => openProcess(record)}>
|
||||
处理
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const submitting =
|
||||
institutionInspNoticeConfirmLoading ||
|
||||
institutionInspNoticeFeedbackLoading ||
|
||||
institutionInspNoticeMaterialUploadLoading;
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={PROTO_THEME}>
|
||||
<PageLayout title="监督检查通知">
|
||||
<div className="inst-page">
|
||||
<div className="inst-page-header">
|
||||
<div className="inst-sub">
|
||||
接收监管端下发的整改通知、监督检查通知、核查函及其他通知文件,机构在线反馈结果、上传证明材料并查看监管复核意见。
|
||||
</div>
|
||||
<div className="inst-tabs">
|
||||
{TAB_ITEMS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
className={`inst-tab${activeTab === tab.key ? " active" : ""}`}
|
||||
onClick={() => switchTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inst-metrics">
|
||||
<div className="inst-metric-card">
|
||||
<div>
|
||||
<div className="label">监管消息总数</div>
|
||||
<div className="value">{metrics.total}</div>
|
||||
<div className="hint">来源:监管端风险预警中心</div>
|
||||
</div>
|
||||
<div className="icon">✉️</div>
|
||||
</div>
|
||||
<div className="inst-metric-card">
|
||||
<div>
|
||||
<div className="label">待处理</div>
|
||||
<div className="value danger">{metrics.pending}</div>
|
||||
<div className="hint">需机构直接处理</div>
|
||||
</div>
|
||||
<div className="icon">待</div>
|
||||
</div>
|
||||
<div className="inst-metric-card">
|
||||
<div>
|
||||
<div className="label">待监管复核</div>
|
||||
<div className="value warning">{metrics.feedback}</div>
|
||||
<div className="hint">材料已提交</div>
|
||||
</div>
|
||||
<div className="icon">审</div>
|
||||
</div>
|
||||
<div className="inst-metric-card">
|
||||
<div>
|
||||
<div className="label">已办结</div>
|
||||
<div className="value success">{metrics.closed}</div>
|
||||
<div className="hint">办结消息可归档</div>
|
||||
</div>
|
||||
<div className="icon">结</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inst-note">
|
||||
<strong>消息来源:</strong>
|
||||
监管端风险预警中心。监管人员在风险预警中心发出整改通知、核查函、补正通知或办结结果后,机构端在此接收并处理;资质保持监控页面只作为监管信息来源。
|
||||
</div>
|
||||
|
||||
<SearchForm
|
||||
form={searchForm}
|
||||
loading={institutionInspNoticePageLoading}
|
||||
style={{ marginBottom: 16 }}
|
||||
formLine={[
|
||||
<Form.Item key="keyword" name="keyword">
|
||||
<ControlWrapper.Input
|
||||
label="通知关键词"
|
||||
placeholder="通知编号/预警编号/事项"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="msgType" name="msgType">
|
||||
<ControlWrapper.Select
|
||||
label="通知类型"
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
options={MSG_TYPE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="statusCode" name="statusCode">
|
||||
<ControlWrapper.Select
|
||||
label="处理状态"
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
options={STATUS_FILTER_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
]}
|
||||
onFinish={() => fetchData(1, pageSize)}
|
||||
onReset={() => {
|
||||
searchForm.resetFields();
|
||||
setActiveTab("all");
|
||||
fetchData(1, pageSize, "all");
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="inst-toolbar">
|
||||
<div className="inst-toolbar-left">
|
||||
<span className="inst-hint">
|
||||
监管消息列表:展示来源预警、来源字段、办理期限和机构处理状态。
|
||||
</span>
|
||||
</div>
|
||||
<div className="inst-toolbar-right">
|
||||
<Button size="small" onClick={() => setExportOpen(true)}>
|
||||
导出消息
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={institutionInspNoticePageLoading}
|
||||
scroll={{ y: props.scrollY, x: 1200 }}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
columnWidth: 34,
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
current: pageIndex,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
}}
|
||||
onChange={(pag) => fetchData(pag.current, pag.pageSize)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 处理 — 对齐原型 regMessageProcess */}
|
||||
<Modal
|
||||
title={detail ? `监管消息处理 - ${detail.noticeNo}` : "监管消息处理"}
|
||||
open={processOpen}
|
||||
onCancel={() => setProcessOpen(false)}
|
||||
width={820}
|
||||
destroyOnClose
|
||||
className="inst-modal"
|
||||
footer={
|
||||
<Button type="primary" loading={submitting} onClick={handleSubmit}>
|
||||
提交
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{institutionInspNoticeGetLoading || !detail ? (
|
||||
<div className="inst-loading">加载中...</div>
|
||||
) : (
|
||||
<>
|
||||
<ModalSection
|
||||
title="处理要求"
|
||||
desc="机构收到消息后直接填写说明并上传证明材料,完成后提交监管复核。"
|
||||
/>
|
||||
<Form form={processForm} layout="vertical" preserve={false}>
|
||||
<div className="inst-form-grid">
|
||||
<Form.Item name="noticeNo" label="关联消息编号">
|
||||
<Input readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item name="processType" label="处理方式">
|
||||
<Select options={PROCESS_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="processRemark"
|
||||
label="处理说明"
|
||||
className="inst-form-full"
|
||||
>
|
||||
<TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item label="上传附件" className="inst-form-full">
|
||||
<Upload
|
||||
name="file"
|
||||
action={UPLOAD_ACTION}
|
||||
headers={{ token: sessionStorage.getItem("token") || "" }}
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
onChange={onFileUpload}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
<div className="inst-upload-tip">
|
||||
支持 PDF、Word、Excel、图片,可选择多个文件。
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 导出 — 对齐原型 regMessageExport */}
|
||||
<Modal
|
||||
title="监管消息清单"
|
||||
open={exportOpen}
|
||||
onCancel={() => setExportOpen(false)}
|
||||
width={560}
|
||||
destroyOnClose
|
||||
className="inst-modal"
|
||||
footer={
|
||||
<Button type="primary" loading={exportLoading} onClick={handleExport}>
|
||||
确认导出
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ModalSection
|
||||
title="导出内容"
|
||||
desc="包含消息编号、来源预警、来源字段、监管要求、办理期限、处理状态和办结结果。"
|
||||
/>
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Connect(
|
||||
[NS_INSP_NOTICE],
|
||||
true,
|
||||
)(AntdTableFuncControl(InstitutionInspectionNotice));
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
.inst-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inst-page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inst-sub {
|
||||
font-size: 0.82rem;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.inst-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inst-tab {
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.inst-tab.active {
|
||||
background: #eff6ff;
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inst-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.inst-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.inst-metric-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inst-metric-card .label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.inst-metric-card .value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 4px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.inst-metric-card .value.danger {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.inst-metric-card .value.warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.inst-metric-card .value.success {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.inst-metric-card .hint {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.inst-metric-card .icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inst-note {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 0.84rem;
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.inst-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inst-hint,
|
||||
.inst-total {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.inst-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inst-loading {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.inst-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.inst-tag-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.inst-tag-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.inst-tag-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.inst-tag-danger {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.inst-modal {
|
||||
.micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.micro-temp-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
padding: 1.5rem;
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.micro-temp-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inst-modal-section {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inst-modal-section h4 {
|
||||
font-size: 0.88rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inst-modal-section > p {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.inst-data-table {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.inst-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
.inst-info-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.inst-info-row.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.inst-info-row .label {
|
||||
flex: 0 0 96px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.inst-info-row .value {
|
||||
flex: 1;
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.inst-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.inst-form-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.inst-source-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.inst-source-pill {
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
.inst-upload-tip {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import OrgInfoGuard from "~/components/OrgInfoGuard";
|
||||
|
||||
export default function InspNoticeManage(props) {
|
||||
return <OrgInfoGuard history={props.history}>{props.children}</OrgInfoGuard>;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,338 @@
|
|||
/* 对齐原型 institution-report-library 圆角体系:
|
||||
* --radius: 8px(按钮/表单)
|
||||
* --radius-lg: 12px(弹窗)
|
||||
* 汇总卡 / 上传区等局部:4~7px
|
||||
*/
|
||||
|
||||
.erl-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.erl-page-desc {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin: -4px 0 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 顶栏汇总 — 原型 .report-library-summary>div { border-radius: 6px } */
|
||||
.erl-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.7rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.erl-summary-item {
|
||||
padding: 0.8rem;
|
||||
background: #fff;
|
||||
border: 1px solid #dce4ec;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.erl-summary-item span,
|
||||
.erl-summary-item strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.erl-summary-item span {
|
||||
font-size: 0.65rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.erl-summary-item strong {
|
||||
font-size: 1.18rem;
|
||||
margin-top: 0.15rem;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 报告名称文件图标 — 原型 .report-file-cell i { border-radius: 4px } */
|
||||
.erl-file-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.erl-file-cell i {
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background: #fff1f2;
|
||||
color: #dc2626;
|
||||
font-size: 0.55rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.erl-file-cell a,
|
||||
.erl-file-cell span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.erl-report-no {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.erl-status-link {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.erl-action-btns {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 原型 .tag { border-radius: 999px } + .tag-success/warning/info/danger */
|
||||
.erl-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.erl-tag-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.erl-tag-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.erl-tag-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.erl-tag-danger {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* 上传弹窗 — 原型 .modal { border-radius: var(--radius-lg)=12px } */
|
||||
.erl-modal-title {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.erl-modal-subtitle {
|
||||
margin-top: 0.2rem;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.erl-upload-modal {
|
||||
.micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 原型单层虚线框,避免 Dragger 内外双边框 */
|
||||
.erl-upload-box {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px dashed #78aee8;
|
||||
border-radius: 7px;
|
||||
background: #f4f9ff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.erl-upload-trigger {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
.micro-temp-upload,
|
||||
.micro-temp-upload-wrapper,
|
||||
.micro-temp-upload-select {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.micro-temp-upload-select {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 清除 antd Upload 默认边框/伪元素,避免左侧边框条与内层重复框 */
|
||||
&::before,
|
||||
&::after,
|
||||
.micro-temp-upload::before,
|
||||
.micro-temp-upload::after,
|
||||
.micro-temp-upload-select::before,
|
||||
.micro-temp-upload-select::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.erl-upload-box-inner {
|
||||
display: grid;
|
||||
grid-template-columns: 54px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 原型 .report-library-upload-icon { border-radius: 6px } */
|
||||
.erl-upload-icon {
|
||||
width: 52px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: #fff1f2;
|
||||
color: #dc2626;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.erl-upload-text strong,
|
||||
.erl-upload-text p,
|
||||
.erl-upload-text span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.erl-upload-text strong {
|
||||
font-size: 0.8rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.erl-upload-text p {
|
||||
margin: 0.18rem 0;
|
||||
font-size: 0.64rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.erl-upload-text span {
|
||||
font-size: 0.65rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* 原型 .report-library-upload-tip { border-radius: 6px } / tip span { 10px } */
|
||||
.erl-upload-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.8rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: #f3f6f9;
|
||||
}
|
||||
|
||||
.erl-upload-tip strong {
|
||||
font-size: 0.68rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.erl-upload-tip span {
|
||||
padding: 0.2rem 0.42rem;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.erl-detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.erl-loading {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.erl-rectify-meta {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.erl-rectify-meta label {
|
||||
color: #64748b;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.erl-rectify-meta > div + div {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* 本页弹窗统一 12px(原型 --radius-lg) */
|
||||
.erl-page ~ div .micro-temp-modal-content,
|
||||
.micro-temp-modal-root .micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.erl-summary {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* 原型 media:.report-library-summary { grid-template-columns: repeat(2,1fr) } */
|
||||
.erl-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.erl-upload-box-inner {
|
||||
grid-template-columns: 54px 1fr;
|
||||
}
|
||||
|
||||
.erl-upload-box-inner > .micro-temp-btn,
|
||||
.erl-upload-box-inner > button {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.erl-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Form,
|
||||
Table,
|
||||
Button,
|
||||
Modal,
|
||||
Space,
|
||||
ConfigProvider,
|
||||
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 {
|
||||
evalPortraitPage,
|
||||
evalPortraitDetail,
|
||||
evalPortraitRules,
|
||||
evalPortraitExport,
|
||||
} from "~/api/regulatorEvalPortrait";
|
||||
import "./index.less";
|
||||
|
||||
/** 对齐原型圆角 */
|
||||
const PROTO_THEME = {
|
||||
token: { borderRadius: 8, borderRadiusLG: 12, borderRadiusSM: 6 },
|
||||
};
|
||||
|
||||
const GRADE_OPTIONS = [
|
||||
{ label: "A级", value: "A级" },
|
||||
{ label: "B级", value: "B级" },
|
||||
{ label: "C级", value: "C级" },
|
||||
];
|
||||
|
||||
const SCOPE_OPTIONS = [
|
||||
{ label: "化工", value: "化工" },
|
||||
{ label: "矿山", value: "矿山" },
|
||||
{ label: "金属冶炼", value: "金属冶炼" },
|
||||
];
|
||||
|
||||
const GRADE_TAG = {
|
||||
A级: "ep-tag-success",
|
||||
B级: "ep-tag-info",
|
||||
C级: "ep-tag-warning",
|
||||
};
|
||||
|
||||
const GRADE_RING = {
|
||||
A级: "score-a",
|
||||
B级: "score-b",
|
||||
C级: "score-c",
|
||||
};
|
||||
|
||||
const ModalSection = ({ title, desc, children }) => (
|
||||
<div className="ep-modal-section">
|
||||
<h4>{title}</h4>
|
||||
{desc ? <p>{desc}</p> : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProtoTable = ({ headers, rows, kv = false }) => (
|
||||
<div className={`ep-data-table${kv ? " ep-data-table-kv" : ""}`}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((h) => (
|
||||
<th key={h}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, idx) => (
|
||||
<tr key={idx}>
|
||||
{row.map((cell, cIdx) => (
|
||||
<td key={cIdx}>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const EvalPortrait = (props) => {
|
||||
const [searchForm] = Form.useForm();
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detail, setDetail] = useState(null);
|
||||
|
||||
const [rulesOpen, setRulesOpen] = useState(false);
|
||||
const [rulesLoading, setRulesLoading] = useState(false);
|
||||
const [rules, setRules] = useState(null);
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
const fetchData = async (page = pageIndex, size = pageSize) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formData = searchForm.getFieldsValue();
|
||||
const res = await evalPortraitPage({
|
||||
...formData,
|
||||
current: page,
|
||||
size,
|
||||
});
|
||||
if (res?.success !== false) {
|
||||
setDataSource(res?.data || []);
|
||||
setTotal(res?.total || 0);
|
||||
setPageIndex(page);
|
||||
setPageSize(size);
|
||||
}
|
||||
} catch {
|
||||
setDataSource([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1, 10);
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchData(1, pageSize);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.resetFields();
|
||||
fetchData(1, pageSize);
|
||||
};
|
||||
|
||||
const openDetail = async (record) => {
|
||||
setDetailOpen(true);
|
||||
setDetail(null);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const res = await evalPortraitDetail({ id: record.id });
|
||||
if (res?.success !== false) {
|
||||
setDetail(res?.data || null);
|
||||
} else {
|
||||
message.error(res?.errMessage || "获取画像失败");
|
||||
}
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openRules = async () => {
|
||||
setRulesOpen(true);
|
||||
setRulesLoading(true);
|
||||
try {
|
||||
const res = await evalPortraitRules();
|
||||
if (res?.success !== false) {
|
||||
setRules(res?.data || null);
|
||||
}
|
||||
} finally {
|
||||
setRulesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!detail?.id) return;
|
||||
setExportLoading(true);
|
||||
try {
|
||||
const res = await evalPortraitExport({ id: detail.id });
|
||||
if (res?.success !== false) {
|
||||
message.success(res?.message || "机构画像已生成");
|
||||
setDetailOpen(false);
|
||||
} else {
|
||||
message.error(res?.errMessage || "导出失败");
|
||||
}
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "评价机构",
|
||||
dataIndex: "orgName",
|
||||
ellipsis: true,
|
||||
width: 240,
|
||||
},
|
||||
{ title: "完成项目", dataIndex: "projectCount", width: 100 },
|
||||
{ title: "按期完成率", dataIndex: "onTimeRate", width: 110 },
|
||||
{ title: "报告抽查合格率", dataIndex: "spotcheckPassRate", width: 130 },
|
||||
{ title: "未闭环预警", dataIndex: "openWarningCount", width: 110 },
|
||||
{
|
||||
title: "监督检查",
|
||||
dataIndex: "supervisionResult",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{ title: "综合得分", dataIndex: "score", width: 100 },
|
||||
{
|
||||
title: "等级",
|
||||
dataIndex: "grade",
|
||||
width: 90,
|
||||
render: (val) => (
|
||||
<span className={`ep-tag ${GRADE_TAG[val] || ""}`}>{val || "-"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 110,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small" onClick={() => openDetail(record)}>
|
||||
查看画像
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={PROTO_THEME}>
|
||||
<PageLayout
|
||||
title="安全评价画像"
|
||||
extra={
|
||||
<Button onClick={openRules}>指标说明</Button>
|
||||
}
|
||||
>
|
||||
<div className="ep-page">
|
||||
<div className="ep-sub">
|
||||
依据机构项目结果、报告质量、按期完成率、预警和监督检查记录形成综合画像。
|
||||
</div>
|
||||
|
||||
<SearchForm
|
||||
form={searchForm}
|
||||
loading={loading}
|
||||
style={{ marginBottom: 16 }}
|
||||
formLine={[
|
||||
<Form.Item key="orgName" name="orgName">
|
||||
<ControlWrapper.Input
|
||||
label="机构名称"
|
||||
placeholder="输入机构名称"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="grade" name="grade">
|
||||
<ControlWrapper.Select
|
||||
label="综合等级"
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
options={GRADE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="businessScope" name="businessScope">
|
||||
<ControlWrapper.Select
|
||||
label="业务范围"
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
options={SCOPE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
]}
|
||||
onFinish={handleSearch}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
scroll={{ y: props.scrollY, x: 1200 }}
|
||||
pagination={{
|
||||
total,
|
||||
current: pageIndex,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
}}
|
||||
onChange={(pag) => {
|
||||
fetchData(pag.current, pag.pageSize);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 查看画像 — 对齐原型 institutionProfile */}
|
||||
<Modal
|
||||
title={detail ? `安全评价画像 - ${detail.orgName}` : "安全评价画像"}
|
||||
open={detailOpen}
|
||||
onCancel={() => setDetailOpen(false)}
|
||||
width={980}
|
||||
destroyOnClose
|
||||
className="ep-modal"
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||
<Button type="primary" loading={exportLoading} onClick={handleExport}>
|
||||
导出画像
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{detailLoading ? (
|
||||
<div className="ep-loading">加载中...</div>
|
||||
) : detail ? (
|
||||
<>
|
||||
<div className="ep-profile-card">
|
||||
<div className={`ep-score-ring ${GRADE_RING[detail.grade] || "score-b"}`}>
|
||||
{detail.grade?.replace("级", "") || detail.score}
|
||||
</div>
|
||||
<div className="ep-profile-info">
|
||||
<h4>{detail.orgName}</h4>
|
||||
<div className="ep-profile-meta">
|
||||
综合得分 <strong>{detail.score}</strong>
|
||||
<span className={`ep-tag ${GRADE_TAG[detail.grade] || ""}`}>
|
||||
{detail.grade}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalSection
|
||||
title="综合画像"
|
||||
desc="画像由项目结果、报告质量、履约时效、风险预警和监督检查数据计算。"
|
||||
>
|
||||
<ProtoTable
|
||||
headers={["指标", "本期结果", "得分", "数据来源"]}
|
||||
rows={(detail.indicators || []).map((item) => [
|
||||
item.name,
|
||||
item.result,
|
||||
item.scoreText,
|
||||
item.source,
|
||||
])}
|
||||
/>
|
||||
</ModalSection>
|
||||
<div className="ep-note">
|
||||
<strong>趋势:</strong>
|
||||
{detail.trend}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="ep-loading">暂无数据</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 指标说明 — 对齐原型 profileRules */}
|
||||
<Modal
|
||||
title={rules?.title || "画像指标说明"}
|
||||
open={rulesOpen}
|
||||
onCancel={() => setRulesOpen(false)}
|
||||
width={720}
|
||||
destroyOnClose
|
||||
className="ep-modal"
|
||||
footer={
|
||||
<Button onClick={() => setRulesOpen(false)}>关闭</Button>
|
||||
}
|
||||
>
|
||||
{rulesLoading ? (
|
||||
<div className="ep-loading">加载中...</div>
|
||||
) : rules ? (
|
||||
<ModalSection title={rules.sectionTitle} desc={rules.sectionDesc}>
|
||||
<ProtoTable
|
||||
headers={["指标", "权重", "计算方式"]}
|
||||
rows={(rules.rows || []).map((item) => [
|
||||
item.name,
|
||||
item.weight,
|
||||
item.method,
|
||||
])}
|
||||
/>
|
||||
</ModalSection>
|
||||
) : (
|
||||
<div className="ep-loading">暂无数据</div>
|
||||
)}
|
||||
</Modal>
|
||||
</PageLayout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AntdTableFuncControl(EvalPortrait);
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
.ep-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ep-sub {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin: -4px 0 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ep-loading {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 原型 .tag */
|
||||
.ep-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.ep-tag-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.ep-tag-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.ep-tag-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* 原型 modal */
|
||||
.ep-modal {
|
||||
.micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.micro-temp-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
padding: 1.5rem;
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.micro-temp-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 原型 .reg-modal-section */
|
||||
.ep-modal-section {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ep-modal-section h4 {
|
||||
font-size: 0.88rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ep-modal-section > p {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 原型 tableHtml */
|
||||
.ep-data-table {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ep-data-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ep-data-table th,
|
||||
.ep-data-table td {
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
color: #334155;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ep-data-table th {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ep-data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ep-data-table-kv td:first-child {
|
||||
width: 140px;
|
||||
color: #64748b;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* 原型 .reg-note */
|
||||
.ep-note {
|
||||
margin-top: 1rem;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #bfdbfe;
|
||||
background: #eff6ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 原型 .profile-card */
|
||||
.ep-profile-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.ep-score-ring {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.score-a {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.score-b {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.score-c {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.score-d {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.ep-profile-info h4 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 0.25rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.ep-profile-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ep-profile-meta strong {
|
||||
color: #2563eb;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,222 @@
|
|||
.redb-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 原型 .training-summary-strip */
|
||||
.redb-summary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
padding: 0.65rem 1rem;
|
||||
margin-bottom: 0.8rem;
|
||||
border: 1px solid #93c5fd;
|
||||
background: #eff8ff;
|
||||
border-radius: 4px;
|
||||
font-size: 0.84rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.redb-summary-strip strong {
|
||||
color: #2563eb;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.redb-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.redb-toolbar-left,
|
||||
.redb-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.redb-total {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.redb-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.redb-loading {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.redb-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.redb-tag-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.redb-tag-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.redb-tag-danger {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* 原型 modal:宽 980、圆角 12、头/底部分割线 */
|
||||
.redb-modal {
|
||||
.micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.micro-temp-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
padding: 1.5rem;
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.micro-temp-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 原型 .reg-modal-section */
|
||||
.redb-modal-section {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.redb-modal-section h4 {
|
||||
font-size: 0.88rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.redb-modal-section > p {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 原型 tableHtml / .data-table */
|
||||
.redb-data-table {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.redb-data-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.redb-data-table th,
|
||||
.redb-data-table td {
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
color: #334155;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.redb-data-table th {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.redb-data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.redb-data-table-kv td:first-child {
|
||||
width: 140px;
|
||||
color: #64748b;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* AI 结论条 — 原型 .reg-note */
|
||||
.redb-ai-note {
|
||||
margin-top: 1rem;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #bfdbfe;
|
||||
background: #eff6ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 原型 .form-grid */
|
||||
.redb-form-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.redb-form-grid .redb-field {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.redb-form-grid .redb-field-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.redb-form-grid .micro-temp-form-item-label > label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.redb-summary-strip {
|
||||
gap: 0.8rem 1.2rem;
|
||||
}
|
||||
|
||||
.redb-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,180 @@
|
|||
.insp-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.insp-sub {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin: -4px 0 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.insp-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.insp-toolbar-left,
|
||||
.insp-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.insp-total {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.insp-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.insp-loading {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.insp-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.insp-tag-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.insp-tag-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.insp-tag-warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.insp-source-pill {
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
.insp-modal {
|
||||
.micro-temp-modal-content {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.micro-temp-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
padding: 1.5rem;
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.micro-temp-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.insp-modal-section {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.insp-modal-section h4 {
|
||||
font-size: 0.88rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.insp-modal-section > p {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.insp-data-table {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.insp-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.insp-form-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.insp-actions {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
Loading…
Reference in New Issue