tangjie 2026-07-28 16:46:43 +08:00
commit 71101bc50a
21 changed files with 10465 additions and 2369 deletions

2
.npmrc
View File

@ -1,2 +1,2 @@
shamefully-hoist=true
hoist=true
node-linker=hoisted

929
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

File diff suppressed because it is too large Load Diff

8
pnpm-workspace.yaml Normal file
View File

@ -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

View File

@ -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",
);

View File

@ -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);

View File

@ -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",
);

View File

@ -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");

View File

@ -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 },
};
}

View File

@ -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">
支持 PDFWordExcel图片可选择多个文件
</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));

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;
}