safety-eval-service-frontend/src/pages/Container/SafetyEvalBusiness/Progress/index.js

323 lines
10 KiB
JavaScript
Raw Normal View History

2026-07-22 10:46:56 +08:00
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";
2026-07-24 16:16:18 +08:00
import { PROGRESS_STATUS_MAP, NODE_LABELS } from "~/enumerate/constant";
2026-07-22 10:46:56 +08:00
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;
2026-07-24 16:16:18 +08:00
const done = nodes.filter((n) => n.state === 1).length;
2026-07-22 10:46:56 +08:00
return (
<div>
<div className="pp-progress-dots">
{nodes.map((node, idx) => {
2026-07-24 16:16:18 +08:00
const dotClass = node.state === 1 ? "pp-progress-dot-done" : "pp-progress-dot-wait";
const name = NODE_LABELS[node.type] || node.type;
2026-07-22 10:46:56 +08:00
return (
2026-07-24 16:16:18 +08:00
<Tooltip key={node.type || idx} title={`${name}: ${node.state === 1 ? "已完成" : "未完成"}`}>
2026-07-22 10:46:56 +08:00
<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));