460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
||
import {
|
||
Tag,
|
||
Button,
|
||
Card,
|
||
Row,
|
||
Col,
|
||
Table,
|
||
Statistic,
|
||
Flex,
|
||
Modal,
|
||
Radio,
|
||
Checkbox,
|
||
message,
|
||
Spin,
|
||
} from "antd";
|
||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
|
||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||
import { ROLE_DEFINITIONS } from "~/enumerate/constant";
|
||
|
||
const { router } = tools;
|
||
|
||
const TeamContent = ({ readOnly, ...props }) => {
|
||
const { safetyEvalBusiness, evalProjectMemberPage, evalProjectMemberSave } =
|
||
props;
|
||
const { evalProjectDetailData, memberLoading, memberSaveLoading } =
|
||
safetyEvalBusiness || {};
|
||
const participants = evalProjectDetailData?.participants || [];
|
||
const projectId = router.query?.id;
|
||
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [selectedRole, setSelectedRole] = useState(null);
|
||
const [selectedPersons, setSelectedPersons] = useState([]);
|
||
const [teamMembers, setTeamMembers] = useState([]);
|
||
|
||
const fetchData = async () => {
|
||
if (!projectId) return;
|
||
const res = await evalProjectMemberPage({ projectId });
|
||
if (res?.success !== false && res?.data) {
|
||
setTeamMembers(
|
||
res.data.map((item) => ({
|
||
...item,
|
||
roles: (item.role || []).map((r) => ({
|
||
role: r,
|
||
duty:
|
||
item.responsibility ||
|
||
ROLE_DEFINITIONS.find((d) => d.value === r)?.summary ||
|
||
"",
|
||
})),
|
||
})),
|
||
);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (evalProjectDetailData?.id) fetchData();
|
||
}, [projectId, evalProjectDetailData?.id]);
|
||
|
||
const isUniqueRole =
|
||
selectedRole &&
|
||
ROLE_DEFINITIONS.find((r) => r.value === selectedRole)?.unique;
|
||
|
||
const handlePersonChange = (checkedValues) => {
|
||
if (isUniqueRole && checkedValues.length > 1) {
|
||
setSelectedPersons([checkedValues[checkedValues.length - 1]]);
|
||
return;
|
||
}
|
||
setSelectedPersons(checkedValues);
|
||
};
|
||
|
||
const handleOpenModal = () => {
|
||
setSelectedRole(null);
|
||
setSelectedPersons([]);
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
if (!selectedRole) {
|
||
message.warning("请选择1个项目角色");
|
||
return;
|
||
}
|
||
if (selectedPersons.length === 0) {
|
||
message.warning("请至少选择1名人员");
|
||
return;
|
||
}
|
||
const role = ROLE_DEFINITIONS.find((r) => r.value === selectedRole);
|
||
if (role?.unique && selectedPersons.length > 1) {
|
||
message.warning("该角色为单人唯一角色,只能选择1名人员");
|
||
return;
|
||
}
|
||
// 唯一角色校验
|
||
if (role?.unique) {
|
||
// 检查是否已有人担任该角色
|
||
const existing = teamMembers.some((m) =>
|
||
m.roles.some((r) => r.role === selectedRole),
|
||
);
|
||
if (existing) {
|
||
message.warning(`"${role.name}"已配置,不可重复添加`);
|
||
return;
|
||
}
|
||
// 一人只能担任一个单人唯一角色
|
||
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,
|
||
)
|
||
);
|
||
});
|
||
if (conflict) {
|
||
const person = participants.find((p) => p.id === conflict);
|
||
message.warning(
|
||
`"${person?.name}"已担任其他唯一角色,一人只能担任一个单人唯一角色`,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const newEntries = selectedPersons.map((personId) => {
|
||
const p = participants.find((item) => item.id === personId);
|
||
const existing = teamMembers.find((m) => m.personnelId === personId);
|
||
return {
|
||
personnelId: p.id,
|
||
personnelName: p.name,
|
||
deptName: p.deptName,
|
||
capacity: p.capacity,
|
||
...(existing?.id ? { id: existing.id } : {}),
|
||
roles: [{ role: selectedRole, duty: role?.summary || "" }],
|
||
};
|
||
});
|
||
|
||
setTeamMembers((prev) => {
|
||
const merged = [...prev];
|
||
newEntries.forEach((entry) => {
|
||
const existing = merged.find(
|
||
(m) => m.personnelId === entry.personnelId,
|
||
);
|
||
if (existing) {
|
||
if (!existing.roles.find((r) => r.role === entry.roles[0].role)) {
|
||
existing.roles.push(entry.roles[0]);
|
||
}
|
||
} else {
|
||
merged.push(entry);
|
||
}
|
||
});
|
||
return merged;
|
||
});
|
||
setModalOpen(false);
|
||
};
|
||
|
||
const handleRemovePerson = (personnelId) => {
|
||
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("请先配置项目负责人(项目组组长)");
|
||
return;
|
||
}
|
||
if (!allRoles.includes("PROCESS_CONTROL_LEAD")) {
|
||
message.warning("请先配置过程控制负责人");
|
||
return;
|
||
}
|
||
const payload = teamMembers.map((m) => ({
|
||
id: m.id,
|
||
projectId,
|
||
personnelId: m.personnelId,
|
||
personnelName: m.personnelName,
|
||
deptName: m.deptName,
|
||
capacity: m.capacity,
|
||
role: m.roles.map((r) => r.role),
|
||
responsibility: m.roles[0]?.duty,
|
||
}));
|
||
const res = await evalProjectMemberSave(payload);
|
||
if (res?.success !== false) {
|
||
message.success("保存成功");
|
||
fetchData();
|
||
props.getDetail();
|
||
}
|
||
};
|
||
|
||
const allRoles = teamMembers.flatMap((m) => m.roles.map((r) => r.role));
|
||
|
||
const columns = [
|
||
{ title: "姓名", dataIndex: "personnelName", width: 120, ellipsis: true },
|
||
{ title: "部门/岗位", dataIndex: "deptName", width: 200, ellipsis: true },
|
||
{
|
||
title: "项目角色",
|
||
dataIndex: "roles",
|
||
width: 350,
|
||
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";
|
||
return (
|
||
<Tag
|
||
key={r.role}
|
||
color={inOnly ? "purple" : "blue"}
|
||
closable={!readOnly}
|
||
onClose={() => {
|
||
setTeamMembers((prev) =>
|
||
prev
|
||
.map((m) => {
|
||
if (m.personnelId !== record.personnelId) return m;
|
||
const updated = m.roles.filter(
|
||
(role) => role.role !== r.role,
|
||
);
|
||
return updated.length > 0
|
||
? { ...m, roles: updated }
|
||
: null;
|
||
})
|
||
.filter(Boolean),
|
||
);
|
||
}}
|
||
>
|
||
{ROLE_DEFINITIONS.find((d) => d.value === r.role)?.name ||
|
||
r.role}
|
||
</Tag>
|
||
);
|
||
})}
|
||
</div>
|
||
),
|
||
},
|
||
{ title: "能力", dataIndex: "capacity", width: 200, ellipsis: true },
|
||
{
|
||
title: "主要职责",
|
||
dataIndex: "roles",
|
||
ellipsis: true,
|
||
render: (roles) => roles.map((r) => r.duty).join(";"),
|
||
},
|
||
{
|
||
title: "操作",
|
||
width: 80,
|
||
fixed: "right",
|
||
render: (_, record) => (
|
||
<Button
|
||
type="link"
|
||
danger
|
||
size="small"
|
||
disabled={readOnly}
|
||
onClick={() => handleRemovePerson(record.personnelId)}
|
||
>
|
||
移除
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Spin spinning={memberLoading}>
|
||
<div>
|
||
<div className="pf-permission">
|
||
<Tag color="blue" className="pf-tag">
|
||
评价部经理 / 机构主账号
|
||
</Tag>
|
||
<span className="pf-desc">
|
||
添加人员时可同时选择多个项目角色和多名人员;普通角色可配置多人。"项目负责人(项目组组长)"和"过程控制负责人"每个项目只能配置1人,选择任一唯一角色时,本次只能选择1名人员。
|
||
</span>
|
||
</div>
|
||
|
||
<Row gutter={16} className="pf-stats-row">
|
||
<Col span={6}>
|
||
<Card size="small">
|
||
<Statistic
|
||
title="已配置人员"
|
||
value={teamMembers.length}
|
||
suffix="人"
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card size="small">
|
||
<Statistic
|
||
title="已覆盖角色"
|
||
value={new Set(allRoles).size + " / 11"}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card size="small">
|
||
<Statistic
|
||
title="项目负责人"
|
||
value={allRoles.includes("PROJECT_LEAD") ? "已配置" : "待配置"}
|
||
valueStyle={{
|
||
color: allRoles.includes("PROJECT_LEAD")
|
||
? "#52c41a"
|
||
: "#faad14",
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card size="small">
|
||
<Statistic
|
||
title="过程控制负责人"
|
||
value={
|
||
allRoles.includes("PROCESS_CONTROL_LEAD")
|
||
? "已配置"
|
||
: "待配置"
|
||
}
|
||
valueStyle={{
|
||
color: allRoles.includes("PROCESS_CONTROL_LEAD")
|
||
? "#52c41a"
|
||
: "#faad14",
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Flex justify="flex-end" className="pf-add-bar">
|
||
{!readOnly && (
|
||
<Button type="primary" size="small" onClick={handleOpenModal}>
|
||
+ 添加人员与角色
|
||
</Button>
|
||
)}
|
||
</Flex>
|
||
|
||
<Table
|
||
dataSource={teamMembers}
|
||
columns={columns}
|
||
rowKey="personnelId"
|
||
pagination={false}
|
||
scroll={{ x: 1600, y: 320 }}
|
||
/>
|
||
|
||
<details className="pf-role-guide">
|
||
<summary className="pf-role-guide-title">
|
||
查看11类项目角色职责说明
|
||
</summary>
|
||
<div className="pf-role-guide-grid">
|
||
{ROLE_DEFINITIONS.map((role) => (
|
||
<div key={role.value} className="pf-role-card">
|
||
<strong className="pf-role-card-title">
|
||
{role.name}
|
||
{role.unique && (
|
||
<Tag color="blue" className="pf-unique-tag">
|
||
单人唯一
|
||
</Tag>
|
||
)}
|
||
</strong>
|
||
<p className="pf-role-card-desc">{role.summary}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</details>
|
||
|
||
<Flex justify="flex-end" gap={8} className="pf-bottom-actions">
|
||
{!readOnly && (
|
||
<Button
|
||
type="primary"
|
||
loading={memberSaveLoading}
|
||
onClick={handleSave}
|
||
>
|
||
确认项目组
|
||
</Button>
|
||
)}
|
||
</Flex>
|
||
|
||
<Modal
|
||
title="添加项目人员与角色"
|
||
open={modalOpen}
|
||
onCancel={() => setModalOpen(false)}
|
||
width={700}
|
||
footer={
|
||
<Flex justify="flex-end" gap={8}>
|
||
<Button onClick={() => setModalOpen(false)}>取消</Button>
|
||
<Button type="primary" onClick={handleConfirm}>
|
||
确认添加
|
||
</Button>
|
||
</Flex>
|
||
}
|
||
>
|
||
<div className="pf-modal-hint">
|
||
{selectedRole &&
|
||
ROLE_DEFINITIONS.find((r) => r.value === selectedRole)?.unique
|
||
? `已选择唯一角色,人员数量限制为1人。当前已选 ${selectedPersons.length} 人。`
|
||
: `已选择 ${selectedRole ? 1 : 0} 个角色、${selectedPersons.length} 名人员。`}
|
||
</div>
|
||
|
||
<div className="pf-modal-section">
|
||
<h4 className="pf-modal-section-title">1. 选择项目角色</h4>
|
||
<Radio.Group
|
||
value={selectedRole}
|
||
onChange={(e) => {
|
||
setSelectedRole(e.target.value);
|
||
setSelectedPersons([]);
|
||
}}
|
||
className="pf-w-full"
|
||
>
|
||
<div className="pf-choice-grid">
|
||
{ROLE_DEFINITIONS.map((role) => (
|
||
<label
|
||
key={role.value}
|
||
className={`pf-choice-card ${selectedRole === role.value ? "pf-choice-card-active" : ""}`}
|
||
>
|
||
<Radio value={role.value} className="pf-hidden-control" />
|
||
<strong className="pf-choice-card-name">
|
||
{role.name}
|
||
{role.unique && (
|
||
<Tag color="blue" className="pf-unique-tag">
|
||
单人唯一
|
||
</Tag>
|
||
)}
|
||
</strong>
|
||
<span className="pf-choice-card-sub">{role.summary}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</Radio.Group>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="pf-modal-section-title">2. 选择人员(可多选)</h4>
|
||
{participants.length === 0 ? (
|
||
<div className="pf-empty">暂无可用人员数据</div>
|
||
) : (
|
||
<Checkbox.Group
|
||
value={selectedPersons}
|
||
onChange={handlePersonChange}
|
||
className="pf-w-full"
|
||
>
|
||
<div className="pf-choice-grid-full">
|
||
{participants.map((person) => (
|
||
<label
|
||
key={person.id}
|
||
className={`pf-choice-card ${selectedPersons.includes(person.id) ? "pf-choice-card-active" : ""}`}
|
||
>
|
||
<Checkbox
|
||
value={person.id}
|
||
className="pf-hidden-control"
|
||
/>
|
||
<strong className="pf-choice-card-name">
|
||
{person.name} · {person.deptName}
|
||
</strong>
|
||
<span className="pf-choice-card-sub">
|
||
{person.posName}
|
||
{person.capacity ? ` · ${person.capacity}` : ""}
|
||
</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</Checkbox.Group>
|
||
)}
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
</Spin>
|
||
);
|
||
};
|
||
|
||
export default Connect([NS_SAFETY_EVAL_BUSINESS], true)(TeamContent);
|