dev-tmp1
tangjie 2026-08-20 15:03:47 +08:00
parent 8b2d97eb47
commit 396a534bf5
3 changed files with 291 additions and 26 deletions

View File

@ -163,6 +163,16 @@ export const evalProjectMemberSave = declareRequest(
"Post > @/safetyEval/institution/eval-project-member/save",
);
export const evalProjectMemberParticipantSave = declareRequest(
"memberParticipantSaveLoading",
"Post > @/safetyEval/institution/eval-project-member/participant/save",
);
export const evalProjectMemberParticipantList = declareRequest(
"memberParticipantListLoading",
"Get > /safetyEval/institution/eval-project-member/participant/list",
);
// ===== 项目预警(安评业务提醒) =====
export const projectWaringStat = declareRequest(

View File

@ -13,6 +13,7 @@ import {
Button,
message,
} from "antd";
import { PictureOutlined } from "@ant-design/icons";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
@ -231,6 +232,25 @@ const TeamPanel = ({ data, loading }) => {
const SurveyPanel = ({ personnel, attachments, loading, detailData }) => {
if (loading) return <Empty />;
/** 打卡时间后追加人脸照片图标faceUrl 逗号分隔第1张为签到、第2张为签退 */
const renderTimeWithFace = (time, record, idx) => {
const url = record?.faceUrl?.split(",")[idx];
if (!url) return time || "-";
return (
<span>
{time}
<Popover
content={<Image src={url} width={240} />}
title="人脸照片"
trigger="hover"
>
<PictureOutlined style={{ marginLeft: 6, color: "#1677ff", cursor: "pointer" }} />
</Popover>
</span>
);
};
const personnelColumns = [
{ title: "人员", dataIndex: "personnelName", width: 100 },
{
@ -263,8 +283,8 @@ const SurveyPanel = ({ personnel, attachments, loading, detailData }) => {
</Tag>
),
},
{ title: "签到时间", dataIndex: "singInTime", width: 140 },
{ title: "签退时间", dataIndex: "signOutTime", width: 140 },
{ title: "签到时间", dataIndex: "singInTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 0);}},
{ title: "签退时间", dataIndex: "signOutTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 1);}},
{
title: "现场照片",
dataIndex: "sceneUrl",

View File

@ -9,6 +9,9 @@ import {
Checkbox,
message,
Spin,
Card,
Popover,
Image,
} from "antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
@ -20,10 +23,22 @@ import LayoutFooterPortal from "~/components/LayoutFooterPortal";
const { router } = tools;
const TeamContent = ({ readOnly, ...props }) => {
const { safetyEvalBusiness, evalProjectMemberPage, evalProjectMemberSave } =
props;
const { evalProjectDetailData, memberLoading, memberSaveLoading } =
safetyEvalBusiness || {};
const {
safetyEvalBusiness,
evalProjectMemberPage,
evalProjectMemberSave,
evalProjectMemberParticipantSave,
evalProjectMemberParticipantList,
evalSignTaskSave,
} = props;
const {
evalProjectDetailData,
memberLoading,
memberSaveLoading,
memberParticipantSaveLoading,
memberParticipantListLoading,
evalSignTaskSaveLoading,
} = safetyEvalBusiness || {};
const participants = evalProjectDetailData?.participants || [];
const projectId = router.query?.id;
@ -31,6 +46,11 @@ const TeamContent = ({ readOnly, ...props }) => {
const [selectedRole, setSelectedRole] = useState(null);
const [selectedPersons, setSelectedPersons] = useState([]);
const [teamMembers, setTeamMembers] = useState([]);
// 参与风险分析人员签字
const [participantModalOpen, setParticipantModalOpen] = useState(false);
const [selectedParticipantIds, setSelectedParticipantIds] = useState([]);
const [signers, setSigners] = useState([]);
const fetchData = async () => {
if (!projectId) return;
@ -49,6 +69,11 @@ const TeamContent = ({ readOnly, ...props }) => {
})),
);
}
// 参与签字人员(查询接口:参与人列表)
const signRes = await evalProjectMemberParticipantList({ projectId });
if (signRes?.success !== false) {
setSigners(signRes?.data || []);
}
};
useEffect(() => {
@ -98,16 +123,15 @@ const TeamContent = ({ readOnly, ...props }) => {
return;
}
// 一人只能担任一个单人唯一角色
const uniqueRoleValues = ROLE_DEFINITIONS.filter(
(r) => r.unique,
).map((r) => r.value);
const uniqueRoleValues = ROLE_DEFINITIONS.filter((r) => r.unique).map(
(r) => r.value,
);
const conflict = selectedPersons.find((personId) => {
const member = teamMembers.find((m) => m.personnelId === personId);
return (
member &&
member.roles.some(
(r) =>
uniqueRoleValues.includes(r.role) && r.role !== selectedRole,
(r) => uniqueRoleValues.includes(r.role) && r.role !== selectedRole,
)
);
});
@ -156,14 +180,68 @@ const TeamContent = ({ readOnly, ...props }) => {
setTeamMembers((prev) => prev.filter((m) => m.personnelId !== personnelId));
};
const handleSave = async () => {
const allRoles = teamMembers.flatMap((m) => m.roles.map((r) => r.role));
if (!allRoles.includes("PROJECT_LEAD")) {
message.warning("请先配置项目负责人(项目组组长)");
// 发起APP签字
const handleSignApp = async (index) => {
const participant = signers[index];
if (!participant?.personnelId) {
message.warning("该人员信息不完整,请先保存后再发起签字");
return;
}
if (!allRoles.includes("PROCESS_CONTROL_LEAD")) {
message.warning("请先配置过程控制负责人");
// 发起签字前先保存签字人员
const saveRes = await evalProjectMemberParticipantSave(
signers.map((s, idx) => ({
id: s.id,
projectId,
personnelId: s.personnelId,
personnelName: s.personnelName,
deptName: s.deptName,
signStatusCode: s.signStatusCode,
signFileUrl: s.signFileUrl,
sortOrder: idx + 1,
})),
);
if (saveRes?.success === false) return;
try {
const res = await evalSignTaskSave({
projectId,
nodeCode: "PROJECT_GROUP",
bizRefId: projectId || undefined,
signerPersonnelId: participant.personnelId,
taskTitle: "项目组成立签字",
taskStatusCode: 1,
});
if (res?.success) {
message.success("已发起APP签字");
setSigners((prev) =>
prev.map((s, i) => (i === index ? { ...s, signStatusCode: 1 } : s)),
);
}
} finally {
}
};
const handleRemoveSigner = (personnelId) => {
setSigners((prev) => prev.filter((s) => s.personnelId !== personnelId));
};
const handleSave = async () => {
const allRoles = teamMembers.flatMap((m) => m.roles.map((r) => r.role));
const requiredRoles = [
{ value: "REPORT_COMPILER", name: "报告编制人" },
{ value: "INTERNAL_AUDITOR", name: "内审人" },
{ value: "PROJECT_LEAD", name: "项目负责人(项目组组长)" },
{ value: "TECHNICAL_LEAD", name: "技术负责人" },
{ value: "PROCESS_CONTROL_LEAD", name: "过程控制负责人" },
];
const missing = requiredRoles.find((r) => !allRoles.includes(r.value));
if (missing) {
message.warning(`请先配置${missing.name}最少1人`);
return;
}
// 校验签字人员是否已完成签字
if (signers.length && !signers.every((s) => s.signStatusCode === 2)) {
message.warning("签字人员尚未全部完成签字,请先完成签字后再确认项目组");
return;
}
const payload = teamMembers.map((m) => ({
@ -178,6 +256,22 @@ const TeamContent = ({ readOnly, ...props }) => {
}));
const res = await evalProjectMemberSave(payload);
if (res?.success !== false) {
// 保存签字人员(保存项目经理签字人)
if (signers.length) {
const signRes = await evalProjectMemberParticipantSave(
signers.map((s, idx) => ({
id: s.id,
projectId,
personnelId: s.personnelId,
personnelName: s.personnelName,
deptName: s.deptName,
signStatusCode: s.signStatusCode,
signFileUrl: s.signFileUrl,
sortOrder: idx + 1,
})),
);
if (signRes?.success === false) return;
}
message.success("保存成功");
fetchData();
props.getDetail();
@ -196,7 +290,8 @@ const TeamContent = ({ readOnly, ...props }) => {
render: (roles, record) => (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{roles.map((r) => {
const inOnly = r.role === "PROJECT_LEAD" || r.role === "PROCESS_CONTROL_LEAD";
const inOnly =
r.role === "PROJECT_LEAD" || r.role === "PROCESS_CONTROL_LEAD";
return (
<Tag
key={r.role}
@ -251,8 +346,69 @@ const TeamContent = ({ readOnly, ...props }) => {
},
];
// 参与风险分析人员签字(去掉分析意见、只能选择一个人)
const signerColumns = [
{ title: "姓名", dataIndex: "personnelName", ellipsis: true },
{ title: "部门/岗位", dataIndex: "deptName", ellipsis: true },
{
title: "签字状态",
render: (_, record, index) => {
const value = record.signStatusCode;
const signFileUrl = record.signFileUrl;
let text = "发起APP签字";
if (value === 1) text = "待APP签字";
if (value === 2) text = "已手写签字";
const btn = (
<Button
size="small"
loading={evalSignTaskSaveLoading || memberParticipantSaveLoading}
disabled={value === 1 || value === 2 || readOnly}
onClick={() => handleSignApp(index)}
>
{text}
</Button>
);
// 已签字且有签字图片时悬停展示签字图片disabled 按钮不触发 hover需用 span 包裹)
if (value === 2 && signFileUrl) {
return (
<Spin spinning={memberLoading}>
<Popover
placement="topRight"
content={
<Image
className="pf-sign-img"
src={signFileUrl}
alt="签字图片"
/>
}
>
<span>{btn}</span>
</Popover>
);
}
return btn;
},
},
{
title: "操作",
width: 80,
render: (_, record) => (
<Button
type="link"
danger
size="small"
disabled={readOnly}
onClick={() => handleRemoveSigner(record.personnelId)}
>
删除
</Button>
),
},
];
return (
<Spin spinning={memberLoading || memberParticipantListLoading}>
<div>
<div className="pf-permission">
<Tag color="blue" className="pf-tag">
@ -272,7 +428,6 @@ const TeamContent = ({ readOnly, ...props }) => {
valueColor: "#1e293b",
iconBg: "#eff6ff",
},
{
key: "roleCoverage",
@ -281,7 +436,6 @@ const TeamContent = ({ readOnly, ...props }) => {
valueColor: "#1677ff",
iconBg: "#e6f4ff",
},
{
key: "projectLead",
@ -290,7 +444,6 @@ const TeamContent = ({ readOnly, ...props }) => {
valueColor: allRoles.includes("PROJECT_LEAD")
? "#52c41a"
: "#faad14",
},
{
key: "processControlLead",
@ -301,7 +454,6 @@ const TeamContent = ({ readOnly, ...props }) => {
valueColor: allRoles.includes("PROCESS_CONTROL_LEAD")
? "#52c41a"
: "#faad14",
},
]}
/>
@ -322,6 +474,35 @@ const TeamContent = ({ readOnly, ...props }) => {
scroll={{ x: 1600, y: 320 }}
/>
<Card
type="inner"
size="small"
title="下发至部门经理或负责人签字"
style={{ marginTop: 16 }}
>
<Table
dataSource={signers}
columns={signerColumns}
rowKey="personnelId"
pagination={false}
scroll={{ x: 800 }}
/>
{!readOnly && (
<Button
type="dashed"
size="small"
style={{ marginTop: 8 }}
disabled={signers.length >= 1}
onClick={() => {
setSelectedParticipantIds([]);
setParticipantModalOpen(true);
}}
>
添加参与人员
</Button>
)}
</Card>
<details className="pf-role-guide">
<summary className="pf-role-guide-title">
查看11类项目角色职责说明
@ -348,7 +529,7 @@ const TeamContent = ({ readOnly, ...props }) => {
<Flex justify="flex-end" gap={8}>
<Button
type="primary"
loading={memberSaveLoading}
loading={memberSaveLoading || memberParticipantSaveLoading}
onClick={handleSave}
>
确认项目组
@ -444,6 +625,60 @@ const TeamContent = ({ readOnly, ...props }) => {
)}
</div>
</Modal>
<Modal
title="选择参与人员"
width={650}
open={participantModalOpen}
onCancel={() => setParticipantModalOpen(false)}
onOk={() => {
if (signers.length >= 1) {
message.warning("该签字人员最多只能配置1人");
setParticipantModalOpen(false);
return;
}
const selected = participants.filter((p) =>
selectedParticipantIds.includes(p.id),
);
selected.forEach((p) => {
setSigners((prev) => {
if (prev.some((s) => s.personnelId === p.id)) return prev;
return [
...prev,
{
personnelId: p.id,
personnelName: p.name,
deptName: p.deptName,
projectId,
},
];
});
});
setParticipantModalOpen(false);
}}
>
<Table
dataSource={(() => {
const existing = signers.map((s) => s.personnelId);
return participants.filter((p) => !existing.includes(p.id));
})()}
rowKey="id"
rowSelection={{
type: "radio",
selectedRowKeys: selectedParticipantIds,
onChange: (keys) => setSelectedParticipantIds(keys),
}}
columns={[
{ title: "姓名", dataIndex: "name", width: 100 },
{ title: "部门", dataIndex: "deptName", width: 120 },
{ title: "岗位", dataIndex: "posName", width: 120 },
{ title: "能力", dataIndex: "capacity", width: 200 },
]}
pagination={false}
size="small"
locale={{ emptyText: "所有人员均已添加" }}
/>
</Modal>
</div>
</Spin>
);