feat
parent
2d20215e96
commit
c96233c0a1
|
|
@ -146,3 +146,23 @@ export const projectWaringRemindPage = declareRequest(
|
|||
"projectWaringRemindPageLoading",
|
||||
"Get > /safetyEval/project-waring/remind-page",
|
||||
);
|
||||
|
||||
// ===== 我的项目统计 =====
|
||||
|
||||
export const evalProjectStatistics = declareRequest(
|
||||
"evalProjectStatisticsLoading",
|
||||
"Get > /safetyEval/institution/eval-project/projectStatistics",
|
||||
'evalProjectStatisticsData: {} | res.data || {}',
|
||||
);
|
||||
|
||||
// ===== 现场踏勘附件 =====
|
||||
|
||||
export const evalSurveyList = declareRequest(
|
||||
"evalSurveyListLoading",
|
||||
"Get > /safetyEval/institution/eval-survey/list",
|
||||
);
|
||||
|
||||
export const evalSurveySaveAttach = declareRequest(
|
||||
"evalSurveySaveAttachLoading",
|
||||
"Post > @/safetyEval/institution/eval-survey/saveSurveyAttach",
|
||||
);
|
||||
|
|
@ -299,7 +299,6 @@ export const ENTERPRISE_SCALE_OPTIONS = [
|
|||
{ label: "微型", value: "MICRO" },
|
||||
];
|
||||
|
||||
|
||||
/** 评价类型选项 */
|
||||
export const EVAL_TYPE_OPTIONS = [
|
||||
{ label: "安全预评价", value: "PRE" },
|
||||
|
|
@ -370,3 +369,14 @@ export const WARING_STATUS_OPTIONS = [
|
|||
{ label: "已逾期", value: "overdue" },
|
||||
{ label: "正常", value: "normal" },
|
||||
];
|
||||
|
||||
export const NODE_LABELS = {
|
||||
RISK_ANALYSIS: "风险分析",
|
||||
BUSINESS_CONTRACT: "商务合同",
|
||||
PROJECT_GROUP: "项目组",
|
||||
SITE_SURVEY: "现场勘查",
|
||||
REPORT_PREPARATION: "报告编制",
|
||||
INTERNAL_REVIEW: "内审",
|
||||
TECHNICAL_REVIEW: "技术审核",
|
||||
PROCESS_CONTROL: "过程控制",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +1,35 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Form, Table, Tag, Button, Card, Row, Col, Statistic, Select, Modal, message,
|
||||
Form, Table, Tag, Button, Card, Row, Col, Statistic, Select, Modal, message, Tooltip,
|
||||
} 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 TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||||
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
|
||||
import { NODE_LABELS } from "~/enumerate/constant";
|
||||
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||||
import "./index.less";
|
||||
|
||||
const { router } = tools;
|
||||
|
||||
const parseProjectNode = (data) => {
|
||||
if (Array.isArray(data)) return data;
|
||||
try {
|
||||
return JSON.parse(data || '[]');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const MyProject = (props) => {
|
||||
const [searchForm] = Form.useForm();
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const { evalProjectLoading } = props.safetyEvalBusiness;
|
||||
const { evalProjectLoading, evalProjectStatisticsData } = props.safetyEvalBusiness;
|
||||
|
||||
const getData = async (pagination) => {
|
||||
const params = {
|
||||
|
|
@ -36,6 +47,7 @@ const MyProject = (props) => {
|
|||
|
||||
useEffect(() => {
|
||||
searchForm.setFieldsValue(router.query);
|
||||
props.evalProjectStatistics();
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
|
|
@ -88,6 +100,30 @@ const MyProject = (props) => {
|
|||
return phase ? <Tag color={color}>{phase}</Tag> : "";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "已完成节点", width: 200,
|
||||
render: (_, record) => {
|
||||
const nodes = parseProjectNode(record.projectNode);
|
||||
if (nodes.length === 0) return <span className="mp-progress-empty">暂无</span>;
|
||||
const total = nodes.length;
|
||||
const done = nodes.filter((n) => n.state === 1).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mp-progress-dots">
|
||||
{nodes.map((n, idx) => (
|
||||
<Tooltip key={n.type || idx} title={NODE_LABELS[n.type] || n.type}>
|
||||
<span className={`mp-progress-dot ${n.state === 1 ? "mp-progress-dot-done" : "mp-progress-dot-wait"}`} />
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<div className="mp-progress-text">
|
||||
{done}/{total} · {Math.round((done / total) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作", width: 140, fixed: "right",
|
||||
render: (_, record) => (
|
||||
|
|
@ -105,25 +141,25 @@ const MyProject = (props) => {
|
|||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="参与项目" value={total} suffix="个" />
|
||||
<Statistic title="参与项目" value={evalProjectStatisticsData.participatingProjectSum ?? "-"} suffix="个" />
|
||||
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>负责人/编制人/审核人</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="待我处理" value={3} valueStyle={{ color: "#faad14" }} suffix="项" />
|
||||
<Statistic title="待我处理" value={evalProjectStatisticsData.pendingMyHandlingSum ?? "-"} valueStyle={{ color: "#faad14" }} suffix="项" />
|
||||
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>当前角色可操作节点</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="现场任务" value={2} valueStyle={{ color: "#52c41a" }} suffix="项" />
|
||||
<Statistic title="现场任务" value={evalProjectStatisticsData.onSiteTaskSum ?? "-"} valueStyle={{ color: "#52c41a" }} suffix="项" />
|
||||
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>需定位打卡和人脸识别</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="延期项目" value={1} valueStyle={{ color: "#ff4d4f" }} suffix="项" />
|
||||
<Statistic title="延期项目" value={evalProjectStatisticsData.delayedProjectSum ?? "-"} valueStyle={{ color: "#ff4d4f" }} suffix="项" />
|
||||
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>需优先处理</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
.mp {
|
||||
&-progress-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-progress-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
||||
&-done { background: #52c41a; }
|
||||
&-wait { background: #e8e8e8; }
|
||||
}
|
||||
|
||||
&-progress-text {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-progress-empty {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ const ContractContent = (props) => {
|
|||
if (res?.success !== false && res?.data) {
|
||||
form.setFieldsValue({
|
||||
...res.data,
|
||||
scanFileUrl: res.data.scanFileUrl? JSON.parse(res.data.scanFileUrl) :undefined,
|
||||
contractStartDate: res.data.contractStartDate
|
||||
? dayjs(res.data.contractStartDate)
|
||||
: undefined,
|
||||
|
|
@ -118,6 +119,7 @@ const ContractContent = (props) => {
|
|||
});
|
||||
if (res?.success !== false) {
|
||||
message.success("合同保存成功");
|
||||
props.getDetail();
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ const RiskAnalysisContent = (props) => {
|
|||
);
|
||||
if (result.success) {
|
||||
message.success(statusCode === 2 ? "风险分析已完成" : "已暂存");
|
||||
props.getDetail()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,65 +1,78 @@
|
|||
import React, { useState } from "react";
|
||||
import { Tag, Button, Card, Table, Flex, Typography, Steps } from "antd";
|
||||
import { CheckCircleFilled } from "@ant-design/icons";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Button, Flex, Typography, Steps, Tag, Card, Table, message } from "antd";
|
||||
import { CheckCircleFilled, DownloadOutlined } from "@ant-design/icons";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
|
||||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||||
import AttachmentUpload from "~/components/AttachmentUpload";
|
||||
import "./index.less";
|
||||
|
||||
const { router } = tools;
|
||||
|
||||
const STAGES = [
|
||||
{ key: "notice", index: "01", label: "从业告知",},
|
||||
{ key: "inspection", index: "02", label: "现场勘查", },
|
||||
{ key: "rectification", index: "03", label: "整改通知", },
|
||||
{ key: "enterprise", index: "04", label: "企业整改",},
|
||||
{ key: "review", index: "05", label: "整改复查", },
|
||||
{ key: "notice", index: "01", label: "从业告知" },
|
||||
{ key: "inspection", index: "02", label: "现场勘查" },
|
||||
{ key: "rectification", index: "03", label: "整改通知" },
|
||||
{ key: "enterprise", index: "04", label: "企业整改" },
|
||||
{ key: "review", index: "05", label: "整改复查" },
|
||||
];
|
||||
|
||||
const ATTACH_TYPE = {
|
||||
notice: "PRACTICE_NOTIFICATION",
|
||||
inspection: "SITE_INSPECTION_FORM",
|
||||
rectification: "RECTIFICATION_NOTICE",
|
||||
enterprise: "ENTERPRISE_RECTIFICATION",
|
||||
review: "RECTIFICATION_REVIEW",
|
||||
};
|
||||
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
const res = await fetch("https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a608cb30dc53ed98d0b13ae.pdf");
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "从业告知书模板.pdf";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
message.error("下载失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
const NoticePanel = () => (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
||||
<div className="pf-section-header">
|
||||
<div>
|
||||
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>01</span>
|
||||
<Typography.Text strong style={{ fontSize: 15 }}>安全评价检测检验机构从业告知书</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>支持在线填写后打印;阶段试行期间,以机构盖章版上传归档作为完成依据。</Typography.Paragraph>
|
||||
</div>
|
||||
<Tag color="success">盖章件已上传</Tag>
|
||||
</div>
|
||||
|
||||
<div style={{ background: "#f0f5ff", padding: "8px 12px", borderRadius: 6, marginBottom: 16, display: "flex", gap: 16, fontSize: 13 }}>
|
||||
<strong>办理方式</strong>
|
||||
<span>在线填写用于生成、打印标准告知书</span>
|
||||
<span>打印盖章后上传扫描件或清晰照片</span>
|
||||
<Tag color="blue">完成节点必须留存盖章件</Tag>
|
||||
</div>
|
||||
|
||||
<Card type="inner" size="small" title="已上传盖章件" extra={<Tag color="success">已上传</Tag>} style={{ marginBottom: 12 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex gap={8} align="center">
|
||||
<span style={{ background: "#1677ff", color: "#fff", padding: "4px 6px", borderRadius: 4, fontSize: 12 }}>PDF</span>
|
||||
<div className="pf-template-card">
|
||||
<div className="pf-template-left">
|
||||
<span className="pf-template-icon">📄</span>
|
||||
<div>
|
||||
<Typography.Text strong>安全评价检测检验机构从业告知书(盖章版).pdf</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ margin: 0, fontSize: 12 }}>2.4MB · 2026-07-07 10:28 上传</Typography.Paragraph>
|
||||
<div className="pf-template-name">从业告知书模板</div>
|
||||
<div className="pf-template-desc">PDF 格式,可下载后在线填写并打印盖章</div>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex gap={8}>
|
||||
<Button size="small">查看</Button>
|
||||
<Button size="small">重新上传</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: "flex", gap: 16, fontSize: 12 }}>
|
||||
<div><CheckCircleFilled style={{ color: "#52c41a" }} /> 机构公章清晰</div>
|
||||
<div><CheckCircleFilled style={{ color: "#52c41a" }} /> 填写内容完整</div>
|
||||
<div><CheckCircleFilled style={{ color: "#52c41a" }} /> 归档版本一致</div>
|
||||
</div>
|
||||
|
||||
<Flex justify="flex-end" gap={8} style={{ marginTop: 16 }}>
|
||||
<Button>打印告知书</Button>
|
||||
<Button type="primary">确认从业告知完成</Button>
|
||||
</Flex>
|
||||
<Button type="primary" ghost size="small" icon={<DownloadOutlined />} onClick={downloadTemplate}>
|
||||
下载模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InspectionPanel = () => (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
||||
<div className="pf-section-header">
|
||||
<div>
|
||||
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>02</span>
|
||||
<Typography.Text strong style={{ fontSize: 15 }}>现场人员打卡与检查表</Typography.Text>
|
||||
|
|
@ -96,132 +109,152 @@ const InspectionPanel = () => (
|
|||
<span>签到日期:2026-07-15</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card type="inner" size="small" title="现场检查表">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex gap={8} align="center">
|
||||
<span style={{ background: "#52c41a", color: "#fff", padding: "4px 6px", borderRadius: 4, fontSize: 12 }}>XLSX</span>
|
||||
<div>
|
||||
<Typography.Text strong>华安化工园区现场安全检查表.xlsx</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ margin: 0, fontSize: 12 }}>1.8MB · 李明华 · 2026-07-15 17:06 上传</Typography.Paragraph>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex gap={8} align="center">
|
||||
<Tag color="success">已归档</Tag>
|
||||
<Button size="small">查看</Button>
|
||||
<Button size="small">删除</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Flex justify="flex-end" gap={8} style={{ marginTop: 16 }}>
|
||||
<Button>刷新打卡状态</Button>
|
||||
<Button>导出打卡记录</Button>
|
||||
<Button type="primary">确认现场勘查完成</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SimpleUploadPanel = ({ index, title, desc, icon, fileType, fileName, fileSize, uploader, date }) => (
|
||||
const SimpleUploadPanel = ({ index, title, desc }) => (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
||||
<div className="pf-section-header">
|
||||
<div>
|
||||
<span style={{ fontSize: 20, fontWeight: 600, color: "#1677ff", marginRight: 8 }}>{index}</span>
|
||||
<Typography.Text strong style={{ fontSize: 15 }}>{title}</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>{desc}</Typography.Paragraph>
|
||||
</div>
|
||||
<Tag color="success">已完成</Tag>
|
||||
</div>
|
||||
|
||||
<Flex gap={16}>
|
||||
<Card style={{ flex: 1, textAlign: "center" }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8 }}>{icon}</div>
|
||||
<Typography.Text strong>上传{title}</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>支持 PDF、Word、JPG、PNG</Typography.Paragraph>
|
||||
<Button type="primary">选择文件</Button>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "#999" }}>上传后本节点自动标记完成</div>
|
||||
</Card>
|
||||
|
||||
<Card style={{ flex: 1 }}>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
|
||||
<Typography.Text strong>已归档文件</Typography.Text>
|
||||
<Tag color="success">已上传</Tag>
|
||||
</Flex>
|
||||
<Flex gap={8} align="center">
|
||||
<span style={{ background: "#1677ff", color: "#fff", padding: "4px 6px", borderRadius: 4, fontSize: 12 }}>{fileType}</span>
|
||||
<div>
|
||||
<Typography.Text strong>{fileName}</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ margin: 0, fontSize: 12 }}>{fileSize} · {uploader} · {date} 上传</Typography.Paragraph>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex justify="flex-end" gap={8} style={{ marginTop: 8 }}>
|
||||
<Button size="small">查看</Button>
|
||||
<Button size="small">替换修改</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="flex-end" gap={8} style={{ marginTop: 16 }}>
|
||||
<Button>重新上传</Button>
|
||||
<Button type="primary">确认完成</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SurveyContent = () => {
|
||||
const [activeStage, setActiveStage] = useState("notice");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pf-permission">
|
||||
<Tag color="blue" className="pf-tag">项目组专职人员 ≥ 2人</Tag>
|
||||
<span className="pf-desc">从业告知、现场勘查、整改与复查全过程留痕</span>
|
||||
</div>
|
||||
|
||||
|
||||
<Steps
|
||||
current={STAGES.findIndex((s) => s.key === activeStage)}
|
||||
|
||||
onChange={(idx) => setActiveStage(STAGES[idx].key)}
|
||||
style={{ marginBottom: 16 }}
|
||||
items={STAGES.map((s) => ({
|
||||
title: s.label,
|
||||
status: "process",
|
||||
}))}
|
||||
/>
|
||||
|
||||
{activeStage === "notice" && <NoticePanel />}
|
||||
{activeStage === "inspection" && <InspectionPanel />}
|
||||
{activeStage === "rectification" && (
|
||||
const STEP_PANELS = {
|
||||
notice: () => <NoticePanel />,
|
||||
inspection: () => <InspectionPanel />,
|
||||
rectification: () => (
|
||||
<SimpleUploadPanel
|
||||
index="03" title="整改通知单上传"
|
||||
desc="本节点不在线编制整改通知,只归档机构已出具的《整改通知单》文件;上传成功即完成,后续可替换修改。"
|
||||
icon="📄" fileType="PDF"
|
||||
fileName="华安化工园区建设项目整改通知单.pdf"
|
||||
fileSize="2.1MB" uploader="张建国" date="2026-07-15 17:20"
|
||||
/>
|
||||
)}
|
||||
{activeStage === "enterprise" && (
|
||||
),
|
||||
enterprise: () => (
|
||||
<SimpleUploadPanel
|
||||
index="04" title="企业整改报告上传"
|
||||
desc="企业线下提交《整改报告》后,由机构人员上传归档;本系统不提供企业在线提交和逐项整改流程。"
|
||||
icon="📄" fileType="PDF"
|
||||
fileName="华安化工园区企业整改报告.pdf"
|
||||
fileSize="3.5MB" uploader="李明华" date="2026-07-16 09:15"
|
||||
/>
|
||||
)}
|
||||
{activeStage === "review" && (
|
||||
),
|
||||
review: () => (
|
||||
<SimpleUploadPanel
|
||||
index="05" title="整改复查单上传"
|
||||
desc="机构人员现场复查后,将经双方签字确认的《整改复查单》上传归档;上传后本节点自动标记完成。"
|
||||
icon="📄" fileType="PDF"
|
||||
fileName="华安化工园区整改复查单(双方签字).pdf"
|
||||
fileSize="1.6MB" uploader="张建国" date="2026-07-18 11:30"
|
||||
/>
|
||||
)}
|
||||
),
|
||||
};
|
||||
|
||||
const SurveyContent = (props) => {
|
||||
const [form] = Form.useForm();
|
||||
const [activeStage, setActiveStage] = useState("notice");
|
||||
const [attachMap, setAttachMap] = useState({});
|
||||
const { evalSurveyListLoading, evalSurveySaveAttachLoading } = props.safetyEvalBusiness;
|
||||
const projectId = router.query?.id;
|
||||
const currentIdx = STAGES.findIndex((s) => s.key === activeStage);
|
||||
const isFirst = currentIdx === 0;
|
||||
const isLast = currentIdx === STAGES.length - 1;
|
||||
|
||||
const loadAttachments = async () => {
|
||||
if (!projectId) return;
|
||||
const res = await props.evalSurveyList({ projectId });
|
||||
if (res?.success !== false) {
|
||||
const map = {};
|
||||
(res?.data || []).forEach((item) => {
|
||||
const key = item.attachTypeCode;
|
||||
try {
|
||||
const parsed = JSON.parse(item.fileUrl);
|
||||
map[key] = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
map[key] = [];
|
||||
}
|
||||
});
|
||||
setAttachMap(map);
|
||||
// 回填所有步骤的附件到表单
|
||||
const values = {};
|
||||
Object.entries(ATTACH_TYPE).forEach(([, typeCode]) => {
|
||||
values[typeCode] = map[typeCode] || [];
|
||||
});
|
||||
form.setFieldsValue(values);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAttachments();
|
||||
}, []);
|
||||
|
||||
const goPrev = () => {
|
||||
if (currentIdx > 0) setActiveStage(STAGES[currentIdx - 1].key);
|
||||
};
|
||||
|
||||
const goNext = () => {
|
||||
if (currentIdx < STAGES.length - 1) setActiveStage(STAGES[currentIdx + 1].key);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
const payload = [];
|
||||
for (const [key, typeCode] of Object.entries(ATTACH_TYPE)) {
|
||||
const fileList = values[typeCode] || [];
|
||||
const validFiles = fileList.filter((f) => f.status === "done" && f.url);
|
||||
if (validFiles.length === 0) {
|
||||
setActiveStage(key);
|
||||
message.warning(`请先在"${STAGES.find((s) => s.key === key).label}"步骤上传附件`);
|
||||
return;
|
||||
}
|
||||
payload.push({
|
||||
projectId,
|
||||
attachTypeCode: typeCode,
|
||||
fileUrl: JSON.stringify(fileList),
|
||||
});
|
||||
}
|
||||
const res = await props.evalSurveySaveAttach(payload);
|
||||
if (res?.success !== false) {
|
||||
message.success("保存成功");
|
||||
props.getDetail();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Steps
|
||||
current={currentIdx}
|
||||
onChange={(idx) => setActiveStage(STAGES[idx].key)}
|
||||
style={{ marginBottom: 16 }}
|
||||
items={STAGES.map((s) => ({ title: s.label }))}
|
||||
/>
|
||||
|
||||
{STEP_PANELS[activeStage]()}
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
{Object.entries(ATTACH_TYPE).map(([key, typeCode]) => (
|
||||
<div key={key} style={{ display: key === activeStage ? "block" : "none" }}>
|
||||
<AttachmentUpload
|
||||
name={typeCode}
|
||||
label="上传附件"
|
||||
maxCount={10}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xls,.xlsx"
|
||||
extra="支持 PDF、Word、JPG、PNG、Excel 格式,最多上传 10 个文件"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Form>
|
||||
|
||||
<Flex justify="flex-end" gap={8} style={{ marginTop: 16 }}>
|
||||
{!isFirst && <Button onClick={goPrev} disabled={evalSurveySaveAttachLoading}>上一步</Button>}
|
||||
{isLast ? (
|
||||
<Button type="primary" onClick={handleSave} loading={evalSurveySaveAttachLoading}>保存</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={goNext} disabled={evalSurveySaveAttachLoading}>下一步</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyContent;
|
||||
export default Connect(
|
||||
[NS_SAFETY_EVAL_BUSINESS],
|
||||
true,
|
||||
)(SurveyContent);
|
||||
|
|
@ -214,9 +214,9 @@ const TeamContent = (props) => {
|
|||
</details>
|
||||
|
||||
<Flex justify="flex-end" gap={8} className="pf-bottom-actions">
|
||||
<Button onClick={handleSave} loading={memberSaveLoading}>暂存</Button>
|
||||
|
||||
<Button>生成项目组任命文件</Button>
|
||||
<Button type="primary">确认项目组</Button>
|
||||
<Button type="primary" onClick={handleSave}>确认项目组</Button>
|
||||
</Flex>
|
||||
|
||||
<Modal title="添加项目人员与角色" open={modalOpen} onCancel={() => setModalOpen(false)} width={700}
|
||||
|
|
|
|||
|
|
@ -21,76 +21,100 @@ const PROCESS_TABS = [
|
|||
key: "risk",
|
||||
index: "1",
|
||||
label: "风险分析",
|
||||
state: "已完成",
|
||||
stateType: "success",
|
||||
role: "市场部经办人 / 市场部经理",
|
||||
},
|
||||
{
|
||||
key: "contract",
|
||||
index: "2",
|
||||
label: "业务合同",
|
||||
state: "已完成",
|
||||
stateType: "success",
|
||||
role: "机构管理员 / 项目负责人",
|
||||
},
|
||||
{
|
||||
key: "team",
|
||||
index: "3",
|
||||
label: "项目组",
|
||||
state: "已完成",
|
||||
stateType: "success",
|
||||
role: "评价部经理 / 机构主账号",
|
||||
},
|
||||
{
|
||||
key: "survey",
|
||||
index: "4",
|
||||
label: "现场踏勘",
|
||||
state: "已完成",
|
||||
stateType: "success",
|
||||
role: "项目组成员",
|
||||
},
|
||||
{
|
||||
key: "report",
|
||||
index: "5",
|
||||
label: "报告编制",
|
||||
state: "编制中",
|
||||
stateType: "processing",
|
||||
role: "报告编制人",
|
||||
},
|
||||
{
|
||||
key: "internal",
|
||||
index: "6",
|
||||
label: "内部审核",
|
||||
state: "待办理",
|
||||
stateType: "default",
|
||||
role: "内部审核人",
|
||||
},
|
||||
{
|
||||
key: "technical",
|
||||
index: "7",
|
||||
label: "技术审核",
|
||||
state: "待办理",
|
||||
stateType: "default",
|
||||
role: "技术负责人",
|
||||
},
|
||||
{
|
||||
key: "control",
|
||||
index: "8",
|
||||
label: "过程控制",
|
||||
state: "前置未完成",
|
||||
stateType: "warning",
|
||||
role: "过程控制负责人",
|
||||
locked: true,
|
||||
},
|
||||
{ key: "team", index: "3", label: "项目组", role: "评价部经理 / 机构主账号" },
|
||||
{ key: "survey", index: "4", label: "现场踏勘", role: "项目组成员" },
|
||||
{ key: "report", index: "5", label: "报告编制", role: "报告编制人" },
|
||||
{ key: "internal", index: "6", label: "内部审核", role: "内部审核人" },
|
||||
{ key: "technical", index: "7", label: "技术审核", role: "技术负责人" },
|
||||
{ key: "control", index: "8", label: "过程控制", role: "过程控制负责人" },
|
||||
];
|
||||
const { router } = tools;
|
||||
|
||||
const TYPE_TO_TAB_KEY = {
|
||||
RISK_ANALYSIS: "risk",
|
||||
BUSINESS_CONTRACT: "contract",
|
||||
PROJECT_GROUP: "team",
|
||||
SITE_SURVEY: "survey",
|
||||
REPORT_PREPARATION: "report",
|
||||
INTERNAL_REVIEW: "internal",
|
||||
TECHNICAL_REVIEW: "technical",
|
||||
PROCESS_CONTROL: "control",
|
||||
};
|
||||
|
||||
const parseProjectNode = (data) => {
|
||||
if (Array.isArray(data)) return data;
|
||||
try {
|
||||
return JSON.parse(data || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildProcessTabs = (projectNode) => {
|
||||
const nodes = parseProjectNode(projectNode);
|
||||
const nodeMap = {};
|
||||
nodes.forEach((n) => {
|
||||
nodeMap[n.type] = n;
|
||||
});
|
||||
|
||||
const allPrevDone = (idx) => {
|
||||
for (let i = 0; i < idx; i++) {
|
||||
const key = PROCESS_TABS[i].key;
|
||||
const type = Object.keys(TYPE_TO_TAB_KEY).find(
|
||||
(k) => TYPE_TO_TAB_KEY[k] === key,
|
||||
);
|
||||
if (!type) continue;
|
||||
const node = nodeMap[type];
|
||||
if (!node || node.state !== 1) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return PROCESS_TABS.map((item, idx) => {
|
||||
const type = Object.keys(TYPE_TO_TAB_KEY).find(
|
||||
(k) => TYPE_TO_TAB_KEY[k] === item.key,
|
||||
);
|
||||
const node = type ? nodeMap[type] : null;
|
||||
|
||||
if (idx === 7) {
|
||||
// 过程控制:前7个全部完成才解锁
|
||||
const unlocked = allPrevDone(7);
|
||||
return {
|
||||
...item,
|
||||
state: unlocked ? "待办理" : "前置未完成",
|
||||
stateType: unlocked ? "default" : "warning",
|
||||
locked: !unlocked,
|
||||
};
|
||||
}
|
||||
|
||||
if (node) {
|
||||
return {
|
||||
...item,
|
||||
state: node.state === 1 ? "已完成" : "待办理",
|
||||
stateType: node.state === 1 ? "success" : "default",
|
||||
locked: false,
|
||||
};
|
||||
}
|
||||
return { ...item, locked: false };
|
||||
});
|
||||
};
|
||||
|
||||
const ProjectFlow = (props) => {
|
||||
const [activeKey, setActiveKey] = useState("risk");
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { wpsDoc } = props.safetyEvalBusiness || {};
|
||||
const { wpsDoc, evalProjectDetailData } = props.safetyEvalBusiness || {};
|
||||
const projectId = router.query?.id;
|
||||
|
||||
const processTabs = buildProcessTabs(evalProjectDetailData?.projectNode);
|
||||
|
||||
const handleTabChange = (key) => {
|
||||
if (key === "report") {
|
||||
openReportEditor();
|
||||
|
|
@ -103,29 +127,33 @@ const ProjectFlow = (props) => {
|
|||
setVisible(true);
|
||||
};
|
||||
|
||||
const getDetail = () => {
|
||||
props.evalProjectDetail({
|
||||
id: projectId,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.queryMyOrCreateDoc({
|
||||
docxUrl: "",
|
||||
});
|
||||
props.evalProjectDetail({
|
||||
id: projectId,
|
||||
});
|
||||
getDetail();
|
||||
}, []);
|
||||
|
||||
const tabItems = PROCESS_TABS.map((item) => {
|
||||
const tabItems = processTabs.map((item) => {
|
||||
let children = null;
|
||||
switch (item.key) {
|
||||
case "risk":
|
||||
children = <RiskAnalysisContent />;
|
||||
children = <RiskAnalysisContent getDetail={getDetail} />;
|
||||
break;
|
||||
case "contract":
|
||||
children = <ContractContent />;
|
||||
children = <ContractContent getDetail={getDetail} />;
|
||||
break;
|
||||
case "team":
|
||||
children = <TeamContent />;
|
||||
children = <TeamContent getDetail={getDetail} />;
|
||||
break;
|
||||
case "survey":
|
||||
children = <SurveyContent />;
|
||||
children = <SurveyContent getDetail={getDetail} />;
|
||||
break;
|
||||
|
||||
case "internal":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,50 @@
|
|||
.pf {
|
||||
// 区块头部
|
||||
&-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafcff;
|
||||
border: 1px solid #e8edf5;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #1677ff;
|
||||
}
|
||||
|
||||
// 模板下载卡片
|
||||
&-template-card {
|
||||
background: #fafcff;
|
||||
border: 1px dashed #1677ff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-template-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&-template-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&-template-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
&-template-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 权限标签区域
|
||||
&-permission {
|
||||
margin-bottom: 12px;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ const RiskWarning = (props) => {
|
|||
width: 100,
|
||||
render: (code) => {
|
||||
const cfg = RISK_LEVEL_MAP[code];
|
||||
return cfg ? <Tag color={cfg.color}>{cfg.text}</Tag> : <Tag>{code}</Tag>;
|
||||
return cfg ? <Tag color={cfg.color}>{cfg.text}</Tag> : "";
|
||||
},
|
||||
},
|
||||
{ title: "预警类型", dataIndex: "warningTypeName", width: 100 },
|
||||
|
|
@ -126,14 +126,14 @@ const RiskWarning = (props) => {
|
|||
width: 260,
|
||||
},
|
||||
{ title: "责任人", dataIndex: "responsiblePerson", width: 90 },
|
||||
{ title: "预警时间", dataIndex: "warningTime", width: 110 },
|
||||
{ title: "预警时间", dataIndex: "warningTime", width: 120 },
|
||||
{
|
||||
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>;
|
||||
return cfg ? <Tag color={cfg.color}>{cfg.text}</Tag> : "";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -242,18 +242,7 @@ const RiskWarning = (props) => {
|
|||
onReset={handleReset}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13, color: "#999" }}>
|
||||
处理按钮会打开处置面板;标记闭环会直接更新行状态。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
|
|
|
|||
Loading…
Reference in New Issue