修改bug

dev-tmp1
huwei 2026-08-14 16:48:24 +08:00
parent a1ce3edda0
commit 7989d62a1c
11 changed files with 1545 additions and 3025 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@ module.exports = {
// API_HOST: "http://192.168.0.134",
// API_HOST: "http://192.168.0.150", //太浅
API_HOST: "https://gbs-gateway.qhdsafety.com",
//API_HOST: "http://192.168.0.103", //huwei
// API_HOST: "https://gbs-gateway.qhdsafety.com",
API_HOST: "http://192.168.0.103", //huwei
},
production: {
// 应用后端分支名称,部署上线需要

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,13 @@ export const reviewSubmit = declareRequest(
"Post > @/safetyEval/qual-filing/review",
);
// 2026-08-14 新增:专家核验(监管端资质备案初审)
// 仅更新专家核验结果(expert_result)与专家审核意见(expert_opinion),不修改备案状态
export const expertReviewSubmit = declareRequest(
"qualReviewSubmitLoading",
"Post > @/safetyEval/qual-filing/expert-review",
);
// ─── 变更模式 ───
export const fetchQualChangeDetail = declareRequest(

View File

@ -49,13 +49,13 @@ export const FILING_STATUS_MAP = {
export const FILING_STATUS_MAP_CHANGE = {
1: { label: "审核通过", color: "success" },
3: { label: "打回", color: "error" },
4: { label: "待审核", color: "processing" },
// 2026-08-14 变更:移除「待审核(4)」展示映射(监管端备案变更管理不再展示待审核)
};
// 2026-08-14 变更:移除「待审核(4)」选项(监管端备案变更管理不再提供待审核筛选)
export const CHANGE_STATUS_OPTIONS = [
{ label: "审核通过", value: 1 },
{ label: "打回", value: 3 },
{ label: "待审核", value: 4 },
];
/** 资质保持监控状态 */

View File

@ -8,6 +8,7 @@ import {
Tag,
Button,
Form,
message,
Modal,
Space,
Typography,
@ -15,6 +16,7 @@ import {
Image,
} from "antd";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { DownloadOutlined } from "@ant-design/icons";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "~/components/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
@ -22,13 +24,123 @@ import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { tools } from "@cqsjjb/jjb-common-lib";
import {
CLOCK_IN_STATUS_OPTIONS,
ROLE_DEFINITIONS_MAP,
} from "~/enumerate/constant";
import "./index.less";
// ============ 变更记录2026-08-14 监管端需求 V2.1 原型):踏勘监管页面改造 ============
// 变更原因:原页面查询项/弹窗与 V2.1 监管平台原型不符,按原型补查询项、改造告知书/打卡弹窗。
// 变更说明:
// 1) 查询项新增"评价机构(orgName 模糊搜索)"、"告知书状态(notificationStatus)"、
// "踏勘日期"(复用后端 plan_end_date入参 planEndDateStart/End)。
// 2) 告知书按钮由直接打开文件改为 Modal 展示核验信息(告知书状态、踏勘日期、归档文件+下载)。
// 3) 打卡弹窗新增"导出打卡记录"按钮(前端 CSV 导出,无需后端接口)。
// ---- 2026-08-14 第二轮prototype-to-code 增量回归,按原型精确对齐)----
// 4) 告知书状态下拉改为原型三态:盖章件完整(COMPLETE)/待上传(PENDING)/超期告知(OVERDUE)。
// 5) 打卡状态下拉改为原型四态:正常(NORMAL)/人员未到齐(NOT_ALL_PRESENT)/缺签到(MISSING_SIGN)/异常(ABNORMAL)。
// 6) 查询项"计划踏勘时间"文案改为原型"踏勘日期起/止"。
// 7) 告知书弹窗标题改为"从业告知详情-{项目名}"(已排除项目组成员/机构盖章/版本一致性及"告知书归档"卡片项)。
// 8) 打卡弹窗补充原型"允许打卡范围"说明;列表表头"实到/应到"改为原型"应到/实到"。
// 影响范围:仅监管端 /MonitorManage/SurveyMonitor 页面。
// 后端同步修改2026-08-14 第二轮已落地):
// - RegulatorEvalProjectSurveyPageQueryorgName、notificationStatus(三态)、clockInStatus(四态)
// - RegulatorEvalProjectSurveyPageCO新增 planEndDatenotificationStatus 枚举为三态;
// - EvalProjectMapper.xml selectRegulatorSurveyPage
// orgName LIKE o.unit_namenotificationStatus 三态OVERDUE 需 plan_end_date<CURDATE
// clockInStatus 四态MISSING_SIGN 映射 NOT_CLOCKED_INABNORMAL 为兜底);
// 主查询返回 plan_end_date
// - RegulatorEvalProjectMonitorExecutor告知书状态统一在 Java 层按三态计算并回填 planEndDate。
// ======================================================================================
const { router } = tools;
// ============ 2026-08-14 第二轮:按原型对齐可选项与弹窗 ============
// 变更原因V2.1 原型查询可选项为【告知状态:盖章件完整/待上传/超期告知】【打卡状态:正常/人员未到齐/缺签到/异常】,
// 与上一轮"已归档/未归档"两态不一致,按原型与用户确认改为三态/四态,并同步修改后端对齐。
// 告知书状态(后端 notificationStatus
// COMPLETE = 盖章件完整(存在 PRACTICE_NOTIFICATION 附件)
// PENDING = 待上传(无附件且未超期)
// OVERDUE = 超期告知(无附件且计划踏勘时间已过,由后端 plan_end_date < 当前日期判定)
// 打卡状态(后端 clockInStatus由 clock_in_status 三态映射为四态):
// NORMAL=正常 NOT_ALL_PRESENT=人员未到齐 MISSING_SIGN=缺签到 ABNORMAL=异常
// ======================================================================
const NOTIFICATION_STATUS_OPTIONS = [
{ label: "盖章件完整", value: "COMPLETE" },
{ label: "待上传", value: "PENDING" },
{ label: "超期告知", value: "OVERDUE" },
];
// 告知书状态前端展示映射(文案/颜色与原型一致)
const NOTIFICATION_STATUS_MAP = {
COMPLETE: { text: "盖章件完整", color: "success" },
PENDING: { text: "待上传", color: "warning" },
OVERDUE: { text: "超期告知", color: "warning" },
};
// 打卡状态下拉选项(对应后端 clockInStatus 四态映射)
const CLOCK_IN_STATUS_FILTER_OPTIONS = [
{ label: "正常", value: "NORMAL" },
{ label: "人员未到齐", value: "NOT_ALL_PRESENT" },
{ label: "缺签到", value: "MISSING_SIGN" },
{ label: "异常", value: "ABNORMAL" },
];
/** 获取告知书归档状态:优先使用后端返回的 notificationStatus兜底按附件与计划时间推断 */
const getNotificationStatus = (record) => {
if (record?.notificationStatus) return record.notificationStatus;
const hasFile = parseFileList(record?.notificationFileUrl).length > 0;
if (hasFile) return "COMPLETE";
// 无附件且计划踏勘时间早于今天 → 超期告知;否则待上传
const planEnd = record?.planEndDate ? new Date(record.planEndDate) : null;
const today = new Date();
if (planEnd && planEnd < today) return "OVERDUE";
return "PENDING";
};
/**
* 2026-08-14 加固归一化告知书状态为原型三态兼容后端可能返回的旧枚举值
* ARCHIVEDCOMPLETE(盖章件完整)NOT_ARCHIVEDPENDING(待上传)未知值兜底 PENDING
*/
const normalizeNotificationStatus = (status) => {
const map = {
ARCHIVED: "COMPLETE",
NOT_ARCHIVED: "PENDING",
COMPLETE: "COMPLETE",
PENDING: "PENDING",
OVERDUE: "OVERDUE",
};
return map[status] || "PENDING";
};
/**
* 2026-08-14 修复解析项目角色字段为数组
* 后端返回的 role JSON 数组字符串 '["PROCESS_CONTROL_LEAD"]'而非数组
* 导致原 Array.isArray 判断失效英文编码原样展示此处统一兼容"数组/JSON字符串/普通字符串"三种形态
*/
const parseRoleList = (v) => {
if (Array.isArray(v)) return v;
if (typeof v === "string") {
const trimmed = v.trim();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
try {
const parsed = JSON.parse(trimmed);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
// 解析失败按逗号拆分兜底
return trimmed.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
}
}
return trimmed ? [trimmed] : [];
}
return [];
};
/** 项目角色编码 → 中文展示(多角色用"、"连接),未匹配到则原样展示 */
const roleText = (v) => {
const list = parseRoleList(v);
return list.map((r) => ROLE_DEFINITIONS_MAP[r] || r).join("、") || "-";
};
const STAT_ITEMS = [
{ key: "notificationArchivedCount", title: "告知书已归档", color: "#1677ff" },
{ key: "clockInNormalCount", title: "人员打卡正常", color: "#52c41a" },
@ -47,15 +159,18 @@ const parseFileList = (raw) => {
}
};
/** 根据打卡数据计算状态 */
/** 2026-08-14 第二轮:根据打卡数据按原型四态计算展示状态(正常/人员未到齐/缺签到/异常) */
const getClockInStatus = (record) => {
// 实到少于应到 → 人员未到齐
if (record.actualCount < record.expectedCount) {
return { color: "error", text: "人员未到齐" };
}
if (
record.signInCount < record.actualCount ||
record.signOutCount < record.actualCount
) {
// 到齐但存在缺签到或缺签退 → 缺签到
if (record.signInCount < record.actualCount || record.signOutCount < record.actualCount) {
return { color: "warning", text: "缺签到" };
}
// 其余异常情况(人脸等未通过)→ 异常
if (record.signInCount === 0 && record.signOutCount === 0) {
return { color: "warning", text: "异常" };
}
return { color: "success", text: "正常" };
@ -68,6 +183,9 @@ const SurveyMonitor = (props) => {
const [clockInModal, setClockInModal] = useState(false);
const [clockInData, setClockInData] = useState([]);
const [clockInProjectName, setClockInProjectName] = useState("");
// 2026-08-14 新增:告知书核验弹窗状态(由直接打开文件改为弹窗展示)
const [noticeModal, setNoticeModal] = useState(false);
const [noticeRecord, setNoticeRecord] = useState(null);
const { safetyEvalBusiness, evalSurveyMonitorStat, evalSurveyMonitorPage,
evalSurveyClockIn } = props;
@ -103,6 +221,14 @@ const SurveyMonitor = (props) => {
const handleSearch = (values) => {
router.query = {
...values,
// 2026-08-14 修复DatePicker 返回 dayjs 对象,需格式化为 yyyy-MM-dd 字符串再传给后端,
// 否则后端 @DateTimeFormat(pattern="yyyy-MM-dd") 无法解析导致踏勘日期筛选失效
planEndDateStart: values.planEndDateStart
? values.planEndDateStart.format("YYYY-MM-DD")
: undefined,
planEndDateEnd: values.planEndDateEnd
? values.planEndDateEnd.format("YYYY-MM-DD")
: undefined,
current: 1,
size: 10,
};
@ -133,13 +259,51 @@ const SurveyMonitor = (props) => {
}
};
// 2026-08-14 改造:告知书由直接打开文件改为弹窗展示核验信息(含归档文件下载)
const handleViewNotice = (record) => {
const files = parseFileList(record.notificationFileUrl);
setNoticeRecord(record);
setNoticeModal(true);
};
// 下载告知书归档文件
const handleDownloadNotice = (record) => {
const files = parseFileList(record?.notificationFileUrl);
if (files.length > 0 && files[0].url) {
window.open(files[0].url, "_blank");
}
};
// 2026-08-14 新增:导出打卡记录(前端 CSV 导出,无需后端接口)
// 字段对应原型打卡弹窗表格:人员/项目角色/人脸识别/ GPS 定位/签到时间/签退时间/现场照片/本人签字
const handleExportClockIn = () => {
if (!clockInData.length) {
message.warning("暂无打卡数据可导出");
return;
}
const header = [
"人员", "项目角色", "人脸识别", "GPS定位", "签到时间", "签退时间", "现场照片", "本人签字",
];
const rows = clockInData.map((row) => [
row.userName || "-",
// 2026-08-14 修复:复用全局 roleText兼容后端 JSON 数组字符串
roleText(row.role),
row.faceState === 1 ? "通过" : "未通过",
row.gpsState === 1 ? "正常" : "异常",
row.singInTime || "-",
row.signOutTime || "-",
row.sceneUrl ? row.sceneUrl.split(",").filter(Boolean).length + "张" : "-",
row.signatureUrl ? "已签字" : "未签字",
]);
// 构造 CSV含 BOM 头,保证 Excel 正确识别中文)
const csv = "" + [header, ...rows].map((r) => r.join(",")).join("\r\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${clockInProjectName || "项目"}_打卡记录.csv`;
link.click();
URL.revokeObjectURL(link.href);
};
const clockInColumns = [
{ title: "人员", dataIndex: "userName", width: 100 },
{
@ -147,10 +311,8 @@ const SurveyMonitor = (props) => {
dataIndex: "role",
width: 160,
ellipsis: true,
render: (v) =>
Array.isArray(v)
? v.map((r) => ROLE_DEFINITIONS_MAP[r] || r).join("、")
: v || "-",
// 2026-08-14 修复:兼容后端 role 为 JSON 数组字符串,映射为中文角色名
render: (v) => roleText(v),
},
{
title: "人脸识别",
@ -213,21 +375,21 @@ const SurveyMonitor = (props) => {
{ title: "评价机构", dataIndex: "orgName", ellipsis: true, width: 200 },
{
title: "告知书状态",
dataIndex: "notificationFileUrl",
dataIndex: "notificationStatus",
width: 120,
render: (v) => {
const files = parseFileList(v);
return files.length > 0 ? (
<Tag color="success">已归档</Tag>
) : (
<Tag color="warning">待上传</Tag>
);
render: (notificationStatus, record) => {
// 2026-08-14 加固:兼容后端旧枚举,统一归一化为原型三态
const raw = notificationStatus || getNotificationStatus(record);
const status = normalizeNotificationStatus(raw);
const map = NOTIFICATION_STATUS_MAP[status] || NOTIFICATION_STATUS_MAP.PENDING;
return <Tag color={map.color}>{map.text}</Tag>;
},
},
{
title: "实到/应到",
// 2026-08-14 第二轮:与原型表头"应到/实到"对齐
title: "应到/实到",
width: 100,
render: (_, r) => `${r.actualCount || 0} / ${r.expectedCount || 0}`,
render: (_, r) => `${r.expectedCount || 0} / ${r.actualCount || 0}`,
},
{
title: "签到/签退",
@ -291,6 +453,7 @@ const SurveyMonitor = (props) => {
loading={false}
style={{ marginBottom: 16 }}
formLine={[
// 项目名称/编号(原型查询项,后端 projectNameOrNo 模糊匹配)
<Form.Item key="projectNameOrNo" name="projectNameOrNo">
<ControlWrapper.Input
label="项目名称/编号"
@ -298,12 +461,37 @@ const SurveyMonitor = (props) => {
allowClear
/>
</Form.Item>,
<Form.Item key="clockInStatus" name="clockInStatus">
// 2026-08-14 新增:评价机构名称模糊查询(后端 orgName对应 org_info.unit_name
<Form.Item key="orgName" name="orgName">
<ControlWrapper.Input
label="评价机构"
placeholder="请输入机构名称"
allowClear
/>
</Form.Item>,
// 2026-08-14 第二轮:告知书状态(后端 notificationStatusCOMPLETE盖章件完整/PENDING待上传/OVERDUE超期告知
<Form.Item key="notificationStatus" name="notificationStatus">
<ControlWrapper.Select
label="打卡状态"
label="告知状态"
placeholder="全部"
allowClear
options={CLOCK_IN_STATUS_OPTIONS}
options={NOTIFICATION_STATUS_OPTIONS}
/>
</Form.Item>,
// 2026-08-14 新增:踏勘日期(复用后端 plan_end_date入参 planEndDateStart/End原型为"踏勘日期起/止"
<Form.Item key="planEndDateStart" name="planEndDateStart">
<ControlWrapper.DatePicker label="踏勘日期起" style={{ width: "100%" }} />
</Form.Item>,
<Form.Item key="planEndDateEnd" name="planEndDateEnd">
<ControlWrapper.DatePicker label="踏勘日期止" style={{ width: "100%" }} />
</Form.Item>,
// 2026-08-14 第二轮打卡状态四态NORMAL正常/NOT_ALL_PRESENT人员未到齐/MISSING_SIGN缺签到/ABNORMAL异常
<Form.Item key="clockInStatus" name="clockInStatus">
<ControlWrapper.Select
label="人员打卡状态"
placeholder="全部"
allowClear
options={CLOCK_IN_STATUS_FILTER_OPTIONS}
/>
</Form.Item>,
]}
@ -332,7 +520,20 @@ const SurveyMonitor = (props) => {
title={`人员打卡详情 - ${clockInProjectName}`}
open={clockInModal}
onCancel={() => setClockInModal(false)}
footer={null}
// 2026-08-14 新增:导出打卡记录按钮(原型要求)
footer={[
<Button
key="export"
icon={<DownloadOutlined />}
onClick={handleExportClockIn}
disabled={!clockInData.length}
>
导出打卡记录
</Button>,
<Button key="close" onClick={() => setClockInModal(false)}>
关闭
</Button>,
]}
width={1000}
>
<Table
@ -344,6 +545,71 @@ const SurveyMonitor = (props) => {
loading={evalSurveyClockInLoading}
scroll={{ x: 900 }}
/>
{/* 2026-08-14 第二轮:按原型补充打卡范围说明 */}
<div style={{ marginTop: 12, color: "#888", fontSize: 13, lineHeight: 1.8 }}>
<strong>允许打卡范围</strong>
项目地址周边500米异常定位缺签到或缺签退将自动进入项目预警
</div>
</Modal>
{/* 2026-08-14 第二轮:告知书弹窗按原型 surveyNotice 还原(标题"从业告知详情";已排除项目组成员/机构盖章/版本一致性及"告知书归档"卡片项) */}
<Modal
title={`从业告知详情 - ${noticeRecord?.projectName || ""}`}
open={noticeModal}
onCancel={() => setNoticeModal(false)}
footer={[
<Button
key="download"
type="primary"
icon={<DownloadOutlined />}
disabled={!parseFileList(noticeRecord?.notificationFileUrl).length}
onClick={() => handleDownloadNotice(noticeRecord)}
>
下载告知书
</Button>,
<Button key="close" onClick={() => setNoticeModal(false)}>
关闭
</Button>,
]}
width={640}
>
{noticeRecord && (() => {
// 2026-08-14 修复兼容后端可能返回的旧枚举值ARCHIVED/NOT_ARCHIVED统一归一化到原型三态避免 undefined 取色报错
const status = normalizeNotificationStatus(getNotificationStatus(noticeRecord));
const statusMap = NOTIFICATION_STATUS_MAP[status] || NOTIFICATION_STATUS_MAP.PENDING;
return (
<>
<Table
dataSource={[
{
key: "status",
item: "告知书状态",
value: <Tag color={statusMap.color}>{statusMap.text}</Tag>,
},
// 踏勘日期:复用后端 plan_end_date原型为"计划踏勘时间"
{
key: "planTime",
item: "计划踏勘时间",
value: noticeRecord.planEndDate || "-",
},
]}
columns={[
{ title: "核验项", dataIndex: "item", width: 140 },
{ title: "内容", dataIndex: "value" },
]}
rowKey="key"
pagination={false}
size="small"
/>
<div style={{ marginTop: 16, color: "#888", fontSize: 13, lineHeight: 1.8 }}>
归档文件
{parseFileList(noticeRecord.notificationFileUrl).length > 0
? "已上传从业告知书,可点击右上角「下载告知书」查看归档文件。"
: "暂未上传从业告知书归档文件。"}
</div>
</>
);
})()}
</Modal>
</PageLayout>
);

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { Button, Form, Select, Table, Tag } from "antd";
import { Button, Form, Select, Table, Tag, Tooltip } from "antd";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "~/components/SearchForm";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
@ -8,11 +8,10 @@ import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { tools } from "@cqsjjb/jjb-common-lib";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_REVIEW } from "~/enumerate/namespace";
import { VERIFY_STATUS_MAP } from "~/enumerate/constant";
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP } from "~/enumerate/enterpriseOptions";
import {
FILING_CODE_TO_CONFIRM,
REVIEW_BUSINESS_SCOPE_OPTIONS,
REVIEW_RESULT_OPTIONS,
REVIEW_STATUS_CLASS,
REVIEW_UNIT_TYPE_OPTIONS,
} from "../reviewPageQuery";
@ -58,20 +57,15 @@ const ExpertVerification = (props) => {
loadList();
}, []);
// 2026-08-14 修复:确认状态列与搜索列对齐(基于 filingStatusCode2待确认/1确认通过/3打回
const renderConfirmStatus = (record) => {
const name = record.filingStatusName || record.statusName;
if (name) {
const color = /待确认|待核验/.test(name)
? "warning"
: /确认通过|已审核|通过/.test(name)
? "success"
: /打回|退回/.test(name)
? "error"
: "default";
return <Tag className={REVIEW_STATUS_CLASS[color] || "tag_default"}>{name}</Tag>;
}
const status = FILING_CODE_TO_CONFIRM[Number(record.filingStatusCode)];
const config = VERIFY_STATUS_MAP[status];
const status = Number(record.filingStatusCode);
const CONF = {
1: { label: "确认通过", color: "success" },
2: { label: "待确认", color: "warning" },
3: { label: "打回", color: "error" },
};
const config = CONF[status];
return config ? (
<Tag className={REVIEW_STATUS_CLASS[config.color] || "tag_default"}>
{config.label}
@ -81,6 +75,28 @@ const ExpertVerification = (props) => {
);
};
// 2026-08-14 调整:专家核验列——优先展示 expert_result(结果),鼠标悬浮 Tooltip 展示 expert_opinion(意见)
// 若结果为空但有意见,则展示意见内容。
const renderExpertCheck = (record) => {
const item = REVIEW_RESULT_OPTIONS.find((r) => r.value === Number(record.expertResult));
const opinion = record.expertOpinion;
// 结果与意见均无 → 显示 "-"
if (!item && !opinion) return "-";
const content = item ? (
<Tag className={REVIEW_STATUS_CLASS[item.value === 1 ? "success" : item.value === 3 ? "error" : "warning"] || "tag_default"}>
{item.label}
</Tag>
) : (
<span className="text-muted">待核验</span>
);
// 有意见时用 Tooltip 悬浮展示完整意见
return opinion ? (
<Tooltip title={opinion} overlayStyle={{ maxWidth: 320 }}>
<span style={{ cursor: "help" }}>{content}</span>
</Tooltip>
) : content;
};
const columns = [
{
title: "序号",
@ -105,6 +121,13 @@ const ExpertVerification = (props) => {
return record.businessScope || "-";
},
},
// 2026-08-14 调整:专家核验(优先展示 expertResult悬浮 Tooltip 展示 expertOpinion
{
title: "专家核验",
dataIndex: "expertResult",
width: 120,
render: (_, record) => renderExpertCheck(record),
},
{
title: "确认状态",
dataIndex: "filingStatusCode",
@ -210,7 +233,7 @@ const ExpertVerification = (props) => {
columns={columns}
dataSource={Array.isArray(qualReviewList) ? qualReviewList : []}
rowSelection={{}}
scroll={{ x: 1000, y: props.scrollY }}
scroll={{ x: 1400, y: props.scrollY }}
loading={qualReviewLoading}
pagination={{
total: qualReviewTotal,

View File

@ -77,7 +77,7 @@ const QualChange = (props) => {
dataIndex: "changeItemNames",
width: 160,
ellipsis: true,
// 后端列表 CO 未聚合变更项(明细在 qual_filing_change_detail.change_item_name待后端补充mock 有值
// 2026-08-14 说明后端已聚合变更项名称qual_filing_change_detail.change_item_name 逗号连接)到 changeItemNames 字段
render: (v) => v || "-",
},
{
@ -102,8 +102,8 @@ const QualChange = (props) => {
>
查看
</Button>
{/* 仅审核中/变更审核中可审核 */}
{[2, 4].includes(Number(record.filingStatusCode)) && (
{/* 2026-08-14 变更:移除「待审核(4)」的审核操作,仅保留审核中(2)可审核 */}
{[2].includes(Number(record.filingStatusCode)) && (
<Button
type="link"
size="small"

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { Button, Form, Select, Table, Tag } from "antd";
import { Button, Form, Select, Table, Tag, Tooltip } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "~/components/SearchForm";
@ -9,9 +9,10 @@ import { tools } from "@cqsjjb/jjb-common-lib";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_REVIEW } from "~/enumerate/namespace";
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP } from "~/enumerate/enterpriseOptions";
import { EXPERT_STATUS_MAP, REVIEW_STATUS_MAP } from "~/enumerate/constant";
import { REVIEW_STATUS_MAP } from "~/enumerate/constant";
import {
REVIEW_BUSINESS_SCOPE_OPTIONS,
REVIEW_RESULT_OPTIONS,
REVIEW_STATUS_CLASS,
REVIEW_UNIT_TYPE_OPTIONS,
} from "../reviewPageQuery";
@ -57,20 +58,28 @@ const QualReview = (props) => {
const renderExpertStatus = (record) => {
if (!record.needExpertCheck) {
return "-";
}
const config = EXPERT_STATUS_MAP[record.expertStatus];
return config ? (
<Tag className={REVIEW_STATUS_CLASS[config.color] || "tag_default"}>
{config.label}
// 2026-08-14 调整:专家核验列——优先展示 expert_result(结果),鼠标悬浮 Tooltip 展示 expert_opinion(意见)
const renderExpertCheck = (record) => {
const item = REVIEW_RESULT_OPTIONS.find((r) => r.value === Number(record.expertResult));
const opinion = record.expertOpinion;
if (!item && !opinion) return "-";
const content = item ? (
<Tag className={REVIEW_STATUS_CLASS[item.value === 1 ? "success" : item.value === 3 ? "error" : "warning"] || "tag_default"}>
{item.label}
</Tag>
) : (
"-"
<span className="text-muted">待核验</span>
);
return opinion ? (
<Tooltip title={opinion} overlayStyle={{ maxWidth: 320 }}>
<span style={{ cursor: "help" }}>{content}</span>
</Tooltip>
) : content;
};
// 2026-08-14 新增:综合审核意见展示
const renderOpinion = (text) => (text ? <span title={text}>{text}</span> : "-");
const columns = [
{
title: "序号",
@ -95,11 +104,12 @@ const QualReview = (props) => {
return record.businessScope || "-";
},
},
// 2026-08-14 调整:专家核验(优先展示 expertResult悬浮 Tooltip 展示 expertOpinion保留一列
{
title: "专家核验",
dataIndex: "expertStatus",
width: 100,
render: (_, record) => renderExpertStatus(record),
dataIndex: "expertResult",
width: 120,
render: (_, record) => renderExpertCheck(record),
},
{
title: "审核状态",
@ -110,6 +120,14 @@ const QualReview = (props) => {
return config ? <Tag color={config.color}>{config.label}</Tag> : '--';
},
},
// 2026-08-14 新增综合审核意见qual_filing_review.review_opinion列表聚合回填
{
title: "综合审核意见",
dataIndex: "reviewOpinion",
width: 200,
ellipsis: true,
render: (text) => renderOpinion(text),
},
{
title: "操作",
width: 140,
@ -217,7 +235,7 @@ const QualReview = (props) => {
columns={columns}
dataSource={Array.isArray(qualReviewList) ? qualReviewList : []}
rowSelection={{}}
scroll={{ x: 1000, y: props.scrollY }}
scroll={{ x: 1500, y: props.scrollY }}
loading={qualReviewLoading}
pagination={{
total: qualReviewTotal,

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect } from "react";
import { Button, Form, Input, message, Modal, Select } from "antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_EXPERT, NS_QUAL_REVIEW } from "~/enumerate/namespace";
@ -7,14 +7,22 @@ import { REVIEW_RESULT_OPTIONS } from "../reviewPageQuery";
const { TextArea } = Input;
/**
* 审核操作弹窗原型 modal-review-action
* - 评价分析已核验材料 / 符合 / 不符合 / 条件未满足 四卡
* - 表单审核结果通过/待定/不通过指派专家综合审核意见
* - 底部取消 / 📤 提交 / 通过
* 审核操作弹窗原型 modal-review-action / modal-expert-review-act
*
* mode 差异化三种审核场景2026-08-14 按原型与需求重构
* 1) 资质备案初审mode 为空审核结果(通过/待审核/不通过) + 指派专家 + 综合审核意见走原 reviewSubmit
* 2) 备案变更管理mode=change审核结果(通过/不通过) + 综合审核意见无指派专家走原 changeReviewSubmit
* 3) 专家资料核验mode=expert核验分析 + 核验意见无审核结果下拉/无指派专家走独立 expertReviewSubmit
*
* 通用头部📊 评价分析已核验材料/符合/不符合/条件未满足 四卡
* 底部取消 / 📤 提交 / 通过
*
* @param {boolean} open
* @param {string|number} filingId - 备案申请ID
* @param {boolean} isChange - 是否变更审核兼容旧调用
* @param {string} mode - 路由 modeexpert=专家核验change=备案变更/其它=资质备案初审
* @param {function} onCancel
* @param {function} onSuccess - 审核成功后回调刷新列表等
* @param {function} onSuccess
*/
const statStyle = {
background: "#fff",
@ -25,11 +33,18 @@ const statStyle = {
};
const statLabelStyle = { fontSize: "0.7rem", color: "#64748b" };
// 2026-08-14 各场景审核结果选项:
// - 资质备案初审:通过/待审核/不通过(后端 1/2/3
// - 备案变更管理:通过/不通过(后端 1/3
const QUAL_RESULT_OPTIONS = REVIEW_RESULT_OPTIONS; // 通过(1)/待审核(2)/不通过(3)
const CHANGE_RESULT_OPTIONS = REVIEW_RESULT_OPTIONS.filter((r) => r.value !== 2); // 通过(1)/不通过(3)
const ReviewModal = (props) => {
const {
open,
filingId,
isChange = false,
mode,
legalPassCount = 0,
legalFailCount = 0,
legalWarnCount = 0,
@ -37,63 +52,111 @@ const ReviewModal = (props) => {
onSuccess,
} = props;
const {
qualExpert,
qualReview,
queryExpertPage,
reviewSubmit,
changeReviewSubmit,
expertReviewSubmit,
} = props;
const { qualExpertList } = qualExpert || {};
// 2026-08-14 专家列表(指派专家下拉数据源,仅资质备案初审场景使用)
const { qualExpertList, queryExpertPage } = props;
const { qualReviewSubmitLoading } = qualReview || {};
// 场景判定:优先按 modemode 为空时按 isChange 兜底
const isExpert = mode === "expert";
const isChangeScene = isChange || mode === "change";
const [form] = Form.useForm();
const reviewResult = Form.useWatch("reviewResult", form);
useEffect(() => {
if (open) {
form.resetFields();
queryExpertPage({ current: 1, size: 100 });
// 2026-08-14 资质备案初审(非专家/非变更)需要指派专家下拉:加载专家列表
if (!isExpert && !isChangeScene) {
queryExpertPage({ current: 1, size: 100 });
}
}
}, [open]);
const expertOptions = useMemo(
() =>
(qualExpertList || []).map((item) => ({
label: item.userName,
value: item.id,
expert: item,
})),
[qualExpertList],
);
// 提取后端错误信息,避免未捕获异常导致 [object Object] 崩溃
const getErrMsg = (err) => {
if (typeof err === "string") return err;
if (err && typeof err === "object") {
return (
err.message ||
err.errMessage ||
err.msg ||
(Array.isArray(err.errors) ? err.errors.map((e) => e.message).join("") : null) ||
"操作失败,请稍后重试"
);
}
return "操作失败,请稍后重试";
};
const doSubmit = async (values, passQuick) => {
const selected = expertOptions.find(
(o) => o.value === values.filingExpertId,
);
// 底部「通过」快捷操作:资质备案初审/备案变更传通过(1),专家核验不区分(直接核验通过)
const result = passQuick ? 1 : values.reviewResult;
const resultLabel =
REVIEW_RESULT_OPTIONS.find((r) => r.value === result)?.label || "通过";
const submitAction = isChange ? changeReviewSubmit : reviewSubmit;
const params = {
filingId,
reviewResult: result,
reviewResultRemark: passQuick ? "审核通过" : resultLabel,
filingExpertId: values.filingExpertId,
filingExpertName: selected?.expert?.userName,
reviewOpinion: values.reviewOpinion || (passQuick ? "审核通过" : ""),
};
if (isChange) {
params.id = params.filingId;
delete params.filingId;
try {
if (isExpert) {
// 专家资料核验:走独立专家核验接口,仅更新专家核验结果(expertResult)与专家审核意见(expertOpinion),不修改备案状态
const expertParams = {
filingId,
expertResult: passQuick ? 1 : result,
expertOpinion: values.expertOpinion || (passQuick ? "核验通过" : ""),
};
const expertRes = await expertReviewSubmit(expertParams);
if (expertRes?.success !== false) {
message.success(passQuick ? "核验通过!" : "专家核验提交成功");
onCancel();
onSuccess?.();
return true;
}
message.error(getErrMsg(expertRes));
return false;
}
// 备案变更管理:走原 change 审核接口(参数 id
if (isChangeScene) {
const params = {
id: filingId,
reviewResult: result,
reviewResultRemark: passQuick ? "审核通过" : (values.reviewResultRemark || ""),
reviewOpinion: values.reviewOpinion || (passQuick ? "审核通过" : ""),
};
const res = await changeReviewSubmit(params);
if (res?.success !== false) {
message.success(passQuick ? "审核通过!" : "审核提交成功");
onCancel();
onSuccess?.();
return true;
}
message.error(getErrMsg(res));
return false;
}
// 资质备案初审:走原 review 审核接口(含指派专家 filingExpertId/filingExpertName
const params = {
filingId,
reviewResult: result,
reviewResultRemark: passQuick ? "审核通过" : (values.reviewResultRemark || ""),
reviewOpinion: values.reviewOpinion || (passQuick ? "审核通过" : ""),
filingExpertId: values.filingExpertId,
filingExpertName: values.filingExpertName,
};
const res = await reviewSubmit(params);
if (res?.success !== false) {
message.success(passQuick ? "审核通过!" : "审核提交成功");
onCancel();
onSuccess?.();
return true;
}
message.error(getErrMsg(res));
return false;
} catch (err) {
message.error(getErrMsg(err));
return false;
}
const res = await submitAction(params);
if (res?.success !== false) {
message.success(passQuick ? "审核通过!" : "审核提交成功");
onCancel();
onSuccess?.();
return true;
}
return false;
};
const handleSubmit = async () => {
@ -110,7 +173,7 @@ const ReviewModal = (props) => {
return (
<Modal
title="审核操作"
title={isExpert ? "专家核验" : "审核操作"}
open={open}
onCancel={onCancel}
width={620}
@ -118,7 +181,7 @@ const ReviewModal = (props) => {
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<Button onClick={onCancel}>取消</Button>
{reviewResult !== 3 && (
<Button loading={qualReviewSubmitLoading} onClick={handlePass} color="green" variant="solid" >
<Button loading={qualReviewSubmitLoading} onClick={handlePass} color="green" variant="solid">
通过
</Button>
)}
@ -132,6 +195,7 @@ const ReviewModal = (props) => {
</div>
}
>
{/* 📊 评价分析(四卡),专家核验场景标题为「核验分析」 */}
<div
style={{
background: "#f8fafc",
@ -148,7 +212,7 @@ const ReviewModal = (props) => {
marginBottom: "0.5rem",
}}
>
📊 评价分析
{isExpert ? "📊 核验分析" : "📊 评价分析"}
</div>
<div
style={{
@ -159,33 +223,25 @@ const ReviewModal = (props) => {
>
<div style={statStyle}>
<div style={statLabelStyle}>已核验材料</div>
<div
style={{ fontSize: "1.1rem", fontWeight: 700, color: "#059669" }}
>
<div style={{ fontSize: "1.1rem", fontWeight: 700, color: "#059669" }}>
{totalChecked}
</div>
</div>
<div style={statStyle}>
<div style={statLabelStyle}>符合</div>
<div
style={{ fontSize: "1.1rem", fontWeight: 700, color: "#059669" }}
>
<div style={{ fontSize: "1.1rem", fontWeight: 700, color: "#059669" }}>
{legalPassCount}
</div>
</div>
<div style={statStyle}>
<div style={statLabelStyle}>不符合</div>
<div
style={{ fontSize: "1.1rem", fontWeight: 700, color: "#dc2626" }}
>
<div style={{ fontSize: "1.1rem", fontWeight: 700, color: "#dc2626" }}>
{legalFailCount}
</div>
</div>
<div style={statStyle}>
<div style={statLabelStyle}>风险提醒</div>
<div
style={{ fontSize: "1.1rem", fontWeight: 700, color: "#d97706" }}
>
<div style={statLabelStyle}>条件未满足</div>
<div style={{ fontSize: "1.1rem", fontWeight: 700, color: "#d97706" }}>
{legalWarnCount}
</div>
</div>
@ -198,45 +254,83 @@ const ReviewModal = (props) => {
style={{ marginTop: "0.5rem" }}
scrollToFirstError
>
<Form.Item
label="审核结果"
name="reviewResult"
rules={[{ required: true, message: "请选择审核结果" }]}
>
<Select placeholder="请选择">
{REVIEW_RESULT_OPTIONS.map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select>
</Form.Item>
{/* 专家核验场景:审核结果 + 核验意见2026-08-14 补充审核结果选项) */}
{isExpert ? (
<>
<Form.Item
label="审核结果"
name="reviewResult"
rules={[{ required: true, message: "请选择审核结果" }]}
>
<Select placeholder="请选择">
{QUAL_RESULT_OPTIONS.map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="核验意见"
name="expertOpinion"
rules={[{ required: true, message: "请输入核验意见" }]}
>
<TextArea rows={3} placeholder="请输入核验意见..." maxLength={200} />
</Form.Item>
</>
) : (
<>
<Form.Item
label="审核结果"
name="reviewResult"
rules={[{ required: true, message: "请选择审核结果" }]}
>
<Select placeholder="请选择">
{(isChangeScene ? CHANGE_RESULT_OPTIONS : QUAL_RESULT_OPTIONS).map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select>
</Form.Item>
{!isChange && (
<Form.Item label="指派专家" name="filingExpertId">
<Select
placeholder="请选择"
showSearch
optionFilterProp="label"
options={expertOptions}
/>
</Form.Item>
{/* 2026-08-14 指派专家:仅资质备案初审显示(原型 modal-review-action备案变更/专家核验不显示 */}
{!isChangeScene && (
<Form.Item
label="指派专家"
name="filingExpertId"
rules={[{ required: true, message: "请选择指派专家" }]}
>
<Select
placeholder="请选择"
showSearch
optionFilterProp="label"
onChange={(_, option) => {
// 同步设置专家姓名,随审核提交给后端
form.setFieldValue("filingExpertName", option?.userName);
}}
>
{(qualExpertList || []).map((exp) => (
<Select.Option key={exp.id} value={exp.id} label={exp.userName}>
{exp.userName}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item
label="综合审核意见"
name="reviewOpinion"
rules={[{ required: true, message: "请输入综合审核意见" }]}
>
<TextArea rows={3} placeholder="请输入综合审核意见..." maxLength={200} />
</Form.Item>
</>
)}
<Form.Item
label="综合审核意见"
name="reviewOpinion"
rules={[{ required: true, message: "请输入综合审核意见" }]}
>
<TextArea
rows={3}
placeholder="请输入综合审核意见..."
maxLength={200}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default Connect([NS_QUAL_EXPERT, NS_QUAL_REVIEW], true)(ReviewModal);
export default Connect([NS_QUAL_REVIEW, NS_QUAL_EXPERT], true)(ReviewModal);

View File

@ -136,6 +136,9 @@ const QualReviewForm = (props) => {
open={reviewVisible}
filingId={qualFilingDetail?.id}
isChange={isChange}
// 2026-08-14 新增mode=expert 表示专家资料核验入口ExpertVerification 进入),
// 此时审核操作走独立专家核验接口,仅更新专家核验结果/意见,不修改备案状态
mode={router.query.mode}
legalPassCount={legalPassCount}
legalFailCount={legalFailCount}
legalWarnCount={legalWarnCount}