2026-07-21 18:03:20 +08:00
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
TreeSelect,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
} from "antd";
|
|
|
|
|
|
import { CloseOutlined } from "@ant-design/icons";
|
|
|
|
|
|
import { NS_STAFF_INFO, NS_ORG_DEPARTMENT } from "~/enumerate/namespace";
|
|
|
|
|
|
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
2026-07-31 16:59:40 +08:00
|
|
|
|
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP, TITLE_LEVEL_MAP } from "~/enumerate/enterpriseOptions";
|
2026-07-21 18:03:20 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const StaffPickerModal = (props) => {
|
|
|
|
|
|
const { open, onCancel, onConfirm, selectedIds = [] } = props;
|
|
|
|
|
|
const { staffInfoList: dataSource, staffInfoTotal: total, staffInfoLoading: loading } = props.staffInfo || {};
|
|
|
|
|
|
const { orgDepartmentTreeData } = props.orgDepartment || {};
|
|
|
|
|
|
|
|
|
|
|
|
const [deptId, setDeptId] = useState(undefined);
|
|
|
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
|
|
|
const [page, setPage] = useState({ current: 1, size: 10 });
|
|
|
|
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
|
|
|
|
// 用 Map 存储所有已选行,翻页后不丢失
|
|
|
|
|
|
const [selectedRowMap, setSelectedRowMap] = useState({});
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
props.orgDepartmentTree();
|
|
|
|
|
|
// 从父组件恢复已选数据
|
|
|
|
|
|
const initialIds = selectedIds;
|
|
|
|
|
|
const initialRows = props.selectedParticipants || [];
|
|
|
|
|
|
setSelectedRowKeys(initialIds);
|
|
|
|
|
|
const map = {};
|
|
|
|
|
|
initialRows.forEach((r) => { map[r.id] = r; });
|
|
|
|
|
|
setSelectedRowMap(map);
|
|
|
|
|
|
fetchData({ current: 1, size: 10 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchData = (pagination) => {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
current: pagination?.current || page.current,
|
|
|
|
|
|
size: pagination?.size || page.size,
|
|
|
|
|
|
employmentStatusCode: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (deptId) params.deptId = deptId;
|
|
|
|
|
|
if (keyword) params.userName = keyword;
|
|
|
|
|
|
props.staffInfoList(params);
|
|
|
|
|
|
setPage(params);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
fetchData({ current: 1, size: 10 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTableChange = (pagination) => {
|
|
|
|
|
|
fetchData(pagination);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectChange = (rowKeys, rows) => {
|
|
|
|
|
|
setSelectedRowKeys(rowKeys);
|
|
|
|
|
|
// rows 仅包含当前页的行,用 Map 合并新旧数据
|
|
|
|
|
|
setSelectedRowMap((prev) => {
|
|
|
|
|
|
const next = { ...prev };
|
|
|
|
|
|
// 移除取消勾选的项
|
|
|
|
|
|
Object.keys(next).forEach((key) => {
|
|
|
|
|
|
if (!rowKeys.includes(key)) {
|
|
|
|
|
|
delete next[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// 添加/更新当前页勾选的行
|
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
|
next[row.id] = row;
|
|
|
|
|
|
});
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRemoveSelected = (record) => {
|
|
|
|
|
|
const newKeys = selectedRowKeys.filter((id) => id !== record.id);
|
|
|
|
|
|
setSelectedRowKeys(newKeys);
|
|
|
|
|
|
setSelectedRowMap((prev) => {
|
|
|
|
|
|
const next = { ...prev };
|
|
|
|
|
|
delete next[record.id];
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleConfirm = () => {
|
|
|
|
|
|
onConfirm(Object.values(selectedRowMap));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectedRows = Object.values(selectedRowMap);
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "用户名称",
|
|
|
|
|
|
dataIndex: "userName",
|
|
|
|
|
|
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
render: (text, record) => {
|
|
|
|
|
|
const name = record.userName || "-";
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Space size={4} wrap align="start">
|
|
|
|
|
|
<span style={{ maxWidth: 100, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "inline-block" }} title={name}>{name}</span>
|
|
|
|
|
|
{record.technicalDirectorFlag && (
|
|
|
|
|
|
<Tag color="blue" style={{ marginRight: 0, fontSize: 11 }}>技术</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{record.processControlLeaderFlag && (
|
|
|
|
|
|
<Tag color="purple" style={{ marginRight: 0, fontSize: 11 }}>过程</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "资质范围",
|
|
|
|
|
|
dataIndex: "qualScope",
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: (code) => {
|
|
|
|
|
|
const status = QUALIFICATION_INDUSTRY_OPTIONS_MAP[code];
|
|
|
|
|
|
return status || "-";
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-07-31 16:59:40 +08:00
|
|
|
|
|
2026-07-21 18:03:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "职称",
|
|
|
|
|
|
dataIndex: "titleCode",
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: (code) => {
|
|
|
|
|
|
const status = TITLE_LEVEL_MAP[code];
|
|
|
|
|
|
return status || "-";
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="选择项目参与人员"
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
onCancel={onCancel}
|
|
|
|
|
|
onOk={handleConfirm}
|
|
|
|
|
|
width={1000}
|
|
|
|
|
|
destroyOnHidden
|
|
|
|
|
|
>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={18}>
|
|
|
|
|
|
<Space style={{ marginBottom: 12, width: "100%" }} size={12}>
|
|
|
|
|
|
<TreeSelect
|
|
|
|
|
|
placeholder="请选择部门"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
treeData={orgDepartmentTreeData}
|
|
|
|
|
|
fieldNames={{ label: "deptName", value: "id", key: "id" }}
|
|
|
|
|
|
treeDefaultExpandAll
|
|
|
|
|
|
showSearch
|
|
|
|
|
|
style={{ width: 240 }}
|
|
|
|
|
|
value={deptId}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
setDeptId(value);
|
|
|
|
|
|
setTimeout(() => handleSearch(), 0);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索用户名称"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
style={{ width: 240 }}
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
|
|
|
|
onPressEnter={handleSearch}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<div style={{ marginBottom: 8, fontSize: 12, color: "#666" }}>
|
|
|
|
|
|
<Tag color="blue" style={{ fontSize: 11, marginRight: 4 }}>技术</Tag> 技术负责人
|
|
|
|
|
|
<Tag color="purple" style={{ fontSize: 11, margin: "0 4px 0 12px" }}>过程</Tag> 过程控制负责人
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys,
|
|
|
|
|
|
preserveSelectedRowKeys: true,
|
|
|
|
|
|
onChange: handleSelectChange,
|
|
|
|
|
|
}}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
total,
|
|
|
|
|
|
current: page.current,
|
|
|
|
|
|
pageSize: page.size,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
|
|
|
|
onChange: (cur, size) => handleTableChange({ current: cur, size }),
|
|
|
|
|
|
}}
|
|
|
|
|
|
scroll={{ y: 400, x: 600 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8 }}>
|
|
|
|
|
|
已选人员({selectedRows.length})
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: "1px solid #d9d9d9",
|
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
|
padding: 8,
|
|
|
|
|
|
minHeight: 300,
|
|
|
|
|
|
maxHeight: 460,
|
|
|
|
|
|
overflow: "auto",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedRows.length === 0 && (
|
|
|
|
|
|
<div style={{ color: "#999", fontSize: 13, textAlign: "center", paddingTop: 120 }}>
|
|
|
|
|
|
请从左侧勾选人员
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedRows.map((item) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
padding: "6px 8px",
|
|
|
|
|
|
borderBottom: "1px solid #f0f0f0",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ fontSize: 13, maxWidth: 140, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={item.userName}>{item.userName}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<CloseOutlined />}
|
|
|
|
|
|
onClick={() => handleRemoveSelected(item)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Connect(
|
|
|
|
|
|
[NS_STAFF_INFO, NS_ORG_DEPARTMENT],
|
|
|
|
|
|
true,
|
|
|
|
|
|
)(StaffPickerModal);
|