dev_1.2
tangjie 2026-07-22 10:46:56 +08:00
parent d1ca41a530
commit f8f241d544
10 changed files with 817 additions and 10 deletions

View File

@ -12,8 +12,8 @@ module.exports = {
// 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095 // 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095
//API_HOST: "https://gbs-gateway.qhdsafety.com", //API_HOST: "https://gbs-gateway.qhdsafety.com",
API_HOST: "http://192.168.0.152", //API_HOST: "http://192.168.0.152",
//API_HOST: "http://192.168.0.150", //太浅 API_HOST: "http://192.168.0.150", //太浅
//API_HOST: "http://192.168.0.152", //API_HOST: "http://192.168.0.152",
//API_HOST: "http://192.168.0.103", //huwei //API_HOST: "http://192.168.0.103", //huwei
}, },

View File

@ -1,6 +0,0 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
export const identifyPartList = declareRequest(
"coursewareLoading",
"Post > @/risk/busIdentifyPart/list",
);

View File

@ -78,3 +78,23 @@ export const evalProjectDocSave = declareRequest(
"evalProjectDocSaveLoading", "evalProjectDocSaveLoading",
"Post > @/safetyEval/institution/eval-project-doc/save", "Post > @/safetyEval/institution/eval-project-doc/save",
); );
export const evalProjectProgressStat = declareRequest(
"evalProjectProgressLoading",
"Get > /safetyEval/institution/eval-project/progress-stat",
);
export const evalProjectProgressPage = declareRequest(
"evalProjectProgressPageLoading",
"Get > /safetyEval/institution/eval-project/progress-page",
);
export const evalProjectRiskWarningStat = declareRequest(
"riskWarningStatLoading",
"Get > /safetyEval/institution/eval-project/risk-warning-stat",
);
export const evalProjectRiskWarningPage = declareRequest(
"riskWarningPageLoading",
"Get > /safetyEval/institution/eval-project/risk-warning-page",
);

View File

@ -332,3 +332,23 @@ export const MATERIAL_TYPE_MAP = MATERIAL_TYPE_OPTIONS.reduce((acc, cur) => {
acc[cur.value] = cur.label; acc[cur.value] = cur.label;
return acc; return acc;
}, {}); }, {});
/** 风险预警 — 风险等级映射 */
export const RISK_LEVEL_MAP = {
HIGH: { color: "error", text: "高风险" },
MIDDLE: { color: "warning", text: "中风险" },
LOW: { color: "default", text: "低风险" },
};
/** 风险预警 — 处理状态映射 */
export const RISK_STATUS_MAP = {
UNHANDLED: { color: "error", text: "未处理" },
PROCESSING: { color: "warning", text: "处理中" },
};
/** 项目进度 — 进度状态映射 */
export const PROGRESS_STATUS_MAP = {
NORMAL: { color: "success", text: "正常" },
NEAR: { color: "warning", text: "项目临期" },
DELAY: { color: "error", text: "项目延期" },
};

View File

@ -212,10 +212,20 @@ const menuItems = [
icon: <FileTextOutlined />, icon: <FileTextOutlined />,
}, },
{ {
key: "/safetyEval/container/SafetyEvalBusiness/EvalProject/ProjectDocLibrary", key: "/safetyEval/container/SafetyEvalBusiness/ProjectDocLibrary",
label: "项目资料管理", label: "项目资料管理",
icon: <FileTextOutlined />, icon: <FileTextOutlined />,
}, },
{
key: "/safetyEval/container/SafetyEvalBusiness/Progress",
label: "项目进度管理",
icon: <BarChartOutlined />,
},
{
key: "/safetyEval/container/SafetyEvalBusiness/RiskWarning",
label: "风险预警管理",
icon: <ExperimentOutlined />,
},
{ {
key: "/safetyEval/container/SafetyEvalBusiness/CustomerManage", key: "/safetyEval/container/SafetyEvalBusiness/CustomerManage",
label: "安评客户管理", label: "安评客户管理",

View File

@ -0,0 +1,326 @@
import React, { useState, useEffect } from "react";
import {
Form,
Table,
Button,
Tag,
Modal,
Row,
Col,
Card,
Statistic,
Descriptions,
Steps,
Tooltip,
} from "antd";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/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 { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { PROGRESS_STATUS_MAP } from "~/enumerate/constant";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { tools } from "@cqsjjb/jjb-common-lib";
import "./index.less";
const { router } = tools;
const ProjectProgress = (props) => {
const [searchForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [statData, setStatData] = useState({});
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [currentRecord, setCurrentRecord] = useState(null);
const { evalProjectProgressPageLoading } = props.safetyEvalBusiness;
const getStat = async () => {
const res = await props.evalProjectProgressStat();
if (res?.success !== false) {
setStatData(res?.data || {});
}
};
const getData = async () => {
const params = {
...router.query,
current: router.query.current || 1,
size: router.query.size || 10,
};
const res = await props.evalProjectProgressPage(params);
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
}
};
useEffect(() => {
searchForm.setFieldsValue(router.query);
getStat();
getData();
}, []);
const handleSearch = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
getData();
};
const handleReset = (values) => {
searchForm.resetFields();
router.query = { ...values, current: 1, size: 10 };
getData();
};
const handlePageChange = (pagination) => {
router.query = {
...router.query,
current: pagination.current,
size: pagination.pageSize,
};
getData();
};
const handleView = (record) => {
setCurrentRecord(record);
setDetailModalVisible(true);
};
const renderProgressDots = (nodes) => {
if (!nodes || nodes.length === 0) return <span className="pp-progress-empty">暂无进度</span>;
const total = nodes.length;
const done = nodes.filter((n) => n.statusCode === 3).length;
return (
<div>
<div className="pp-progress-dots">
{nodes.map((node, idx) => {
const dotClass =
node.statusCode === 3 ? "pp-progress-dot-done" :
node.statusCode === 2 ? "pp-progress-dot-doing" :
node.statusCode === 4 ? "pp-progress-dot-error" :
"pp-progress-dot-wait";
return (
<Tooltip key={node.nodeCode || idx} title={`${node.nodeName}: ${node.statusName}`}>
<span className={`pp-progress-dot ${dotClass}`} />
</Tooltip>
);
})}
</div>
<div className="pp-progress-text">
{done}/{total} · {Math.round((done / total) * 100)}%
</div>
</div>
);
};
const columns = [
{ title: "项目编号", dataIndex: "projectNo", width: 100 },
{
title: "客户/项目",
width: 180,
render: (_, record) => (
<div>
<div className="pp-project-name">{record.projectName}</div>
<div className="pp-customer-name">{record.customerName}</div>
</div>
),
},
{ title: "评价类别", dataIndex: "evalTypeName", width: 110 },
{ title: "负责人", dataIndex: "projectLeaderName", width: 100, ellipsis: true },
{
title: "项目进度",
width: 240,
render: (_, record) => renderProgressDots(record.projectNode),
},
{ title: "项目结束日期", dataIndex: "planEndDate", width: 120 },
{
title: "项目状态",
dataIndex: "progressStatusCode",
width: 130,
render: (code, record) => {
const cfg = PROGRESS_STATUS_MAP[code];
const daysText =
record.remainingDays >= 0
? `剩余${record.remainingDays}`
: `超期${Math.abs(record.remainingDays)}`;
return (
<div>
<div>{cfg ? <Tag color={cfg.color}>{cfg.text}-{daysText}</Tag> : <Tag>{code}</Tag>}</div>
</div>
);
},
},
{
title: "操作",
width: 80,
fixed: "right",
render: (_, record) => (
<Button type="link" size="small" onClick={() => handleView(record)}>
查看
</Button>
),
},
];
return (
<PageLayout title="项目进度管理">
<Row gutter={16} className="pp-stat-row">
<Col span={6}>
<Card size="small">
<Statistic
title="项目总数"
value={statData.totalCount ?? "-"}
suffix={<span className="pp-stat-suffix">当前机构名下评价项目</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="正常项目"
value={statData.normalCount ?? "-"}
valueStyle={{ color: "#52c41a" }}
suffix={<span className="pp-stat-suffix">距项目结束日期超过3天</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="项目临期"
value={statData.nearExpiryCount ?? "-"}
valueStyle={{ color: "#faad14" }}
suffix={<span className="pp-stat-suffix">项目结束前3天自动提醒</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="项目延期"
value={statData.delayedCount ?? "-"}
valueStyle={{ color: "#ff4d4f" }}
suffix={<span className="pp-stat-suffix">已超过项目结束日期</span>}
/>
</Card>
</Col>
</Row>
<SearchForm
form={searchForm}
loading={false}
style={{ marginBottom: 16 }}
formLine={[
<Form.Item key="keyword" name="keyword">
<ControlWrapper.Input
label="项目名称/客户名称"
placeholder="项目名称/客户名称"
allowClear
/>
</Form.Item>,
<Form.Item key="progressStatus" name="progressStatus">
<ControlWrapper.Select
label="项目状态"
placeholder="全部"
allowClear
options={[
{ label: "正常", value: "NORMAL" },
{ label: "项目临期", value: "NEAR" },
{ label: "项目延期", value: "DELAY" },
]}
/>
</Form.Item>,
]}
onFinish={handleSearch}
onReset={handleReset}
/>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={evalProjectProgressPageLoading}
scroll={{ y: props.scrollY, x: 1100 }}
pagination={{
total,
current: router.query.current || 1,
pageSize: router.query.size || 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
onChange={handlePageChange}
/>
<Modal
title="项目进度详情"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={null}
width={680}
destroyOnHide={true}
>
{currentRecord && (
<>
<Descriptions column={2} bordered size="small" className="pp-detail-desc">
<Descriptions.Item label="项目编号">{currentRecord.projectNo}</Descriptions.Item>
<Descriptions.Item label="项目名称">{currentRecord.projectName}</Descriptions.Item>
<Descriptions.Item label="客户名称">{currentRecord.customerName}</Descriptions.Item>
<Descriptions.Item label="客户负责人">{currentRecord.customerContactName}</Descriptions.Item>
<Descriptions.Item label="评价类型">{currentRecord.evalTypeName}</Descriptions.Item>
<Descriptions.Item label="项目负责人">{currentRecord.projectLeaderName}</Descriptions.Item>
<Descriptions.Item label="项目结束日期">{currentRecord.planEndDate}</Descriptions.Item>
<Descriptions.Item label="剩余天数">
<span
className={`pp-remaining-days ${
currentRecord.remainingDays < 0 ? "pp-remaining-danger" :
currentRecord.remainingDays <= 3 ? "pp-remaining-warn" :
"pp-remaining-normal"
}`}
>
{currentRecord.remainingDays >= 0 ? `${currentRecord.remainingDays}` : `已超期${Math.abs(currentRecord.remainingDays)}`}
</span>
</Descriptions.Item>
<Descriptions.Item label="项目状态" span={2}>
<Tag color={PROGRESS_STATUS_MAP[currentRecord.progressStatusCode]?.color}>
{currentRecord.progressStatusName}
</Tag>
</Descriptions.Item>
</Descriptions>
<div className="pp-node-title">项目节点进度</div>
<Steps
direction="vertical"
current={-1}
items={
currentRecord.projectNode?.map((node) => ({
title: node.nodeName,
status:
node.statusCode === 3
? "finish"
: node.statusCode === 2
? "process"
: node.statusCode === 4
? "error"
: "wait",
description: node.statusName,
})) || []
}
/>
</>
)}
</Modal>
</PageLayout>
);
};
export default Connect(
[NS_SAFETY_EVAL_BUSINESS],
true,
)(AntdTableFuncControl(ProjectProgress));

View File

@ -0,0 +1,72 @@
.pp {
// 统计卡片区域
&-stat-row {
margin-bottom: 16px;
}
&-stat-suffix {
font-size: 12px;
color: #999;
}
// 进度圆点容器
&-progress-dots {
display: flex;
gap: 4px;
align-items: center;
}
&-progress-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
cursor: pointer;
&-done { background: #52c41a; }
&-doing { background: #1677ff; }
&-error { background: #ff4d4f; }
&-wait { background: #e8e8e8; }
}
&-progress-text {
font-size: 11px;
color: #999;
margin-top: 4px;
}
&-progress-empty {
color: #999;
font-size: 12px;
}
// 客户/项目列
&-project-name {
font-weight: 500;
}
&-customer-name {
font-size: 12px;
color: #999;
}
// 详情弹窗 - Descriptions 间距
&-detail-desc {
margin-bottom: 24px;
}
// 剩余天数
&-remaining-days {
font-weight: 600;
}
&-remaining-normal { color: #52c41a; }
&-remaining-warn { color: #faad14; }
&-remaining-danger { color: #ff4d4f; }
// 节点进度标题
&-node-title {
font-weight: 600;
margin-bottom: 16px;
font-size: 15px;
}
}

View File

@ -0,0 +1,336 @@
import React, { useState, useEffect } from "react";
import {
Form,
Table,
Button,
Tag,
Modal,
Row,
Col,
Card,
Statistic,
Space,
message,
} from "antd";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/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 { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { RISK_LEVEL_MAP, RISK_STATUS_MAP } from "~/enumerate/constant";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { tools } from "@cqsjjb/jjb-common-lib";
import "./index.less";
const { router } = tools;
const RiskWarning = (props) => {
const [searchForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [statData, setStatData] = useState({});
const [handleModalVisible, setHandleModalVisible] = useState(false);
const [currentRecord, setCurrentRecord] = useState(null);
const { riskWarningPageLoading } = props.safetyEvalBusiness;
const getStat = async () => {
const res = await props.evalProjectRiskWarningStat();
if (res?.success !== false) {
setStatData(res?.data || {});
}
};
const getData = async () => {
const params = {
...router.query,
current: router.query.current || 1,
size: router.query.size || 10,
};
const res = await props.evalProjectRiskWarningPage(params);
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
}
};
useEffect(() => {
searchForm.setFieldsValue(router.query);
getStat();
getData();
}, []);
const handleSearch = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
getData();
};
const handleReset = (values) => {
searchForm.resetFields();
router.query = { ...values, current: 1, size: 10 };
getData();
};
const handlePageChange = (pagination) => {
router.query = {
...router.query,
current: pagination.current,
size: pagination.pageSize,
};
getData();
};
const handleProcess = (record) => {
setCurrentRecord(record);
setHandleModalVisible(true);
};
const handleCloseRisk = async (record) => {
Modal.confirm({
title: "确认闭环",
content: `确定将"${record.projectName}"的风险标记为闭环吗?`,
onOk: async () => {
// TODO: 调用闭环接口
message.success("已闭环");
getData();
},
});
};
const columns = [
{ title: "项目编号", dataIndex: "projectNo", width: 130 },
{
title: "项目/客户",
width: 240,
render: (_, record) => (
<div>
<div className="rw-project-name">{record.projectName}</div>
<div className="rw-customer-name">{record.customerName}</div>
</div>
),
},
{
title: "风险等级",
dataIndex: "riskLevelCode",
width: 100,
render: (code) => {
const cfg = RISK_LEVEL_MAP[code];
return cfg ? <Tag color={cfg.color}>{cfg.text}</Tag> : <Tag>{code}</Tag>;
},
},
{ title: "预警类型", dataIndex: "warningTypeName", width: 100 },
{
title: "触发条件",
dataIndex: "triggerCondition",
ellipsis: true,
width: 260,
},
{ title: "责任人", dataIndex: "responsiblePerson", width: 90 },
{ title: "预警时间", dataIndex: "warningTime", width: 110 },
{
title: "状态",
dataIndex: "statusCode",
width: 90,
render: (code) => {
const cfg = RISK_STATUS_MAP[code];
return cfg ? <Tag color={cfg.color}>{cfg.text}</Tag> : <Tag>{code}</Tag>;
},
},
{
title: "操作",
width: 130,
fixed: "right",
render: (_, record) => (
<Space>
<Button type="link" size="small" onClick={() => handleProcess(record)}>
处理
</Button>
<Button type="link" size="small" onClick={() => handleCloseRisk(record)}>
闭环
</Button>
</Space>
),
},
];
return (
<PageLayout title="风险预警管理">
<Row gutter={16} className="rw-stat-row">
<Col span={6}>
<Card size="small">
<Statistic
title="预警总数"
value={statData.totalCount ?? "-"}
suffix={<span className="rw-stat-suffix">来自项目过程控制与周期规则</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="高风险"
value={statData.highRiskCount ?? "-"}
valueStyle={{ color: "#ff4d4f" }}
suffix={<span className="rw-stat-suffix">延期逾期重大整改</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="处理中"
value={statData.processingCount ?? "-"}
valueStyle={{ color: "#faad14" }}
suffix={<span className="rw-stat-suffix">已分派但未闭环</span>}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="本周新增"
value={statData.weekNewCount ?? "-"}
valueStyle={{ color: "#52c41a" }}
suffix={<span className="rw-stat-suffix">需在周例会确认</span>}
/>
</Card>
</Col>
</Row>
<SearchForm
form={searchForm}
loading={false}
style={{ marginBottom: 16 }}
formLine={[
<Form.Item key="projectOrCustomer" name="projectOrCustomer">
<ControlWrapper.Input
label="项目/客户"
placeholder="项目或客户名称"
allowClear
/>
</Form.Item>,
<Form.Item key="riskLevel" name="riskLevel">
<ControlWrapper.Select
label="风险等级"
placeholder="全部"
allowClear
options={[
{ label: "高风险", value: "HIGH" },
{ label: "中风险", value: "MIDDLE" },
{ label: "低风险", value: "LOW" },
]}
/>
</Form.Item>,
<Form.Item key="warningType" name="warningType">
<ControlWrapper.Select
label="预警类型"
placeholder="全部"
allowClear
options={[
{ label: "进度风险", value: "PROGRESS" },
{ label: "周期风险", value: "CYCLE" },
{ label: "质量风险", value: "QUALITY" },
{ label: "人员风险", value: "PERSONNEL" },
{ label: "归档风险", value: "ARCHIVE" },
]}
/>
</Form.Item>,
]}
onFinish={handleSearch}
onReset={handleReset}
/>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<span style={{ fontSize: 13, color: "#999" }}>
处理按钮会打开处置面板标记闭环会直接更新行状态
</span>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={riskWarningPageLoading}
scroll={{ y: props.scrollY, x: 1300 }}
pagination={{
total,
current: router.query.current || 1,
pageSize: router.query.size || 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
onChange={handlePageChange}
/>
<Modal
title="风险处置"
open={handleModalVisible}
onCancel={() => setHandleModalVisible(false)}
footer={null}
width={600}
destroyOnHide={true}
>
{currentRecord && (
<div>
<p>
<strong>项目</strong>
{currentRecord.projectName}
</p>
<p>
<strong>风险等级</strong>
<Tag color={RISK_LEVEL_MAP[currentRecord.riskLevelCode]?.color}>
{currentRecord.riskLevelName}
</Tag>
</p>
<p>
<strong>预警类型</strong>
{currentRecord.warningTypeName}
</p>
<p>
<strong>触发条件</strong>
{currentRecord.triggerCondition}
</p>
<p>
<strong>责任人</strong>
{currentRecord.responsiblePerson}
</p>
<p>
<strong>预警时间</strong>
{currentRecord.warningTime}
</p>
<div style={{ textAlign: "right", marginTop: 24 }}>
<Space>
<Button onClick={() => setHandleModalVisible(false)}>取消</Button>
<Button
type="primary"
onClick={() => {
message.success("处理完成");
setHandleModalVisible(false);
getData();
}}
>
确认处理
</Button>
</Space>
</div>
</div>
)}
</Modal>
</PageLayout>
);
};
export default Connect(
[NS_SAFETY_EVAL_BUSINESS],
true,
)(AntdTableFuncControl(RiskWarning));

View File

@ -0,0 +1,29 @@
.rw {
&-stat-row {
margin-bottom: 16px;
}
&-stat-suffix {
font-size: 12px;
color: #999;
}
&-note {
padding: 8px 12px;
background: #f6f8fa;
border-radius: 6px;
font-size: 13px;
color: #555;
margin-bottom: 16px;
border: 1px solid #e8e8e8;
}
&-project-name {
font-weight: 500;
}
&-customer-name {
font-size: 12px;
color: #999;
}
}