2025-06-23 21:39:51 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
2025-06-23 19:15:13 +08:00
|
|
|
|
import { Input, Button, Badge, Avatar, Space, Tabs, Checkbox, Row, Col, Card, Empty, Divider, Alert } from 'antd';
|
|
|
|
|
import { SearchOutlined, UserOutlined, TeamOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
|
|
|
|
import './EvaluateTaskPersonnelSelector.less';
|
|
|
|
|
|
|
|
|
|
const { TabPane } = Tabs;
|
|
|
|
|
const { Search } = Input;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 评价任务人员选择组件属性接口
|
|
|
|
|
* @interface EvaluateTaskPersonnelSelectorProps
|
|
|
|
|
* @property {Function} onSelect - 选择确认后的回调函数,返回所选人员数组
|
|
|
|
|
* @property {PersonnelItem[]} selectedPersonnel - 已选择的人员列表
|
|
|
|
|
*/
|
|
|
|
|
interface EvaluateTaskPersonnelSelectorProps {
|
|
|
|
|
onSelect: (personnel: PersonnelItem[]) => void;
|
|
|
|
|
selectedPersonnel?: PersonnelItem[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 人员信息接口
|
|
|
|
|
* @interface PersonnelItem
|
|
|
|
|
* @property {string} id - 人员唯一标识
|
|
|
|
|
* @property {string} name - 人员姓名
|
|
|
|
|
* @property {string} department - 所属部门
|
|
|
|
|
* @property {string} position - 职位信息
|
|
|
|
|
* @property {boolean} selected - 是否被选中
|
|
|
|
|
*/
|
|
|
|
|
export interface PersonnelItem {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
department: string;
|
|
|
|
|
position?: string;
|
|
|
|
|
selected?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 部门信息接口
|
|
|
|
|
* @interface Department
|
|
|
|
|
* @property {string} id - 部门ID
|
|
|
|
|
* @property {string} name - 部门名称
|
|
|
|
|
*/
|
|
|
|
|
interface Department {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 评价任务人员选择组件
|
|
|
|
|
* 用于在评价任务管理中选择评价人员,按部门分类展示
|
|
|
|
|
*
|
|
|
|
|
* @component
|
|
|
|
|
* @example
|
|
|
|
|
* ```jsx
|
|
|
|
|
* <EvaluateTaskPersonnelSelector
|
|
|
|
|
* onSelect={(selected) => console.log('已选择人员:', selected)}
|
|
|
|
|
* selectedPersonnel={[]}
|
|
|
|
|
* />
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
const EvaluateTaskPersonnelSelector: React.FC<EvaluateTaskPersonnelSelectorProps> = ({
|
|
|
|
|
onSelect,
|
|
|
|
|
selectedPersonnel = []
|
|
|
|
|
}) => {
|
|
|
|
|
// 搜索关键词
|
|
|
|
|
const [keyword, setKeyword] = useState<string>('');
|
|
|
|
|
// 所有可选人员列表
|
|
|
|
|
const [personnel, setPersonnel] = useState<PersonnelItem[]>([]);
|
|
|
|
|
// 已选择人员ID列表
|
|
|
|
|
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
|
|
|
|
// 当前选中的部门Tab
|
|
|
|
|
const [activeTab, setActiveTab] = useState<string>('1');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 部门数据列表
|
|
|
|
|
* 实际项目中可通过API获取
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const departments: Department[] = useMemo(() => [
|
2025-06-23 19:15:13 +08:00
|
|
|
|
{ id: '1', name: '采购部' },
|
|
|
|
|
{ id: '2', name: '技术部' },
|
|
|
|
|
{ id: '3', name: '质量部' },
|
|
|
|
|
{ id: '4', name: '生产部' },
|
|
|
|
|
{ id: '5', name: '财务部' },
|
2025-06-23 21:39:51 +08:00
|
|
|
|
], []);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 模拟获取人员数据
|
|
|
|
|
* 实际项目中替换为API调用
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const fetchPersonnelData = useCallback(() => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
// 模拟API调用获取评价人员
|
|
|
|
|
const mockPersonnel: PersonnelItem[] = [
|
|
|
|
|
{ id: '1', name: '张三', department: '采购部', position: '采购经理' },
|
|
|
|
|
{ id: '2', name: '李四', department: '技术部', position: '技术专家' },
|
|
|
|
|
{ id: '3', name: '王五', department: '质量部', position: '质量工程师' },
|
|
|
|
|
{ id: '4', name: '赵六', department: '采购部', position: '高级采购' },
|
|
|
|
|
{ id: '5', name: '钱七', department: '技术部', position: '部门经理' },
|
|
|
|
|
{ id: '6', name: '孙八', department: '质量部', position: '质量总监' },
|
|
|
|
|
{ id: '7', name: '周九', department: '生产部', position: '生产主管' },
|
|
|
|
|
{ id: '8', name: '吴十', department: '财务部', position: '财务经理' },
|
|
|
|
|
{ id: '9', name: '郑十一', department: '采购部', position: '采购专员' },
|
|
|
|
|
{ id: '10', name: '王十二', department: '技术部', position: '工程师' },
|
|
|
|
|
{ id: '11', name: '刘十三', department: '财务部', position: '财务专员' },
|
|
|
|
|
{ id: '12', name: '陈十四', department: '生产部', position: '技术员' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
setPersonnel(mockPersonnel);
|
2025-06-23 21:39:51 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化人员数据和已选状态
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// 加载人员数据(模拟API请求)
|
|
|
|
|
fetchPersonnelData();
|
|
|
|
|
|
|
|
|
|
// 设置已选人员
|
|
|
|
|
if (selectedPersonnel && selectedPersonnel.length > 0) {
|
|
|
|
|
const selectedIds = selectedPersonnel.map(item => item.id);
|
|
|
|
|
setSelectedKeys(selectedIds);
|
|
|
|
|
}
|
|
|
|
|
}, [fetchPersonnelData, selectedPersonnel]);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理搜索
|
|
|
|
|
* @param {string} value - 搜索关键词
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const handleSearch = useCallback((value: string) => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
setKeyword(value);
|
2025-06-23 21:39:51 +08:00
|
|
|
|
}, []);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理Tab切换
|
|
|
|
|
* @param {string} key - 选中的Tab键值
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const handleTabChange = useCallback((key: string) => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
setActiveTab(key);
|
2025-06-23 21:39:51 +08:00
|
|
|
|
}, []);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理单个人员选择
|
|
|
|
|
* @param {string} personnelId - 人员ID
|
|
|
|
|
* @param {boolean} checked - 是否选中
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const handleSelect = useCallback((personnelId: string, checked: boolean) => {
|
|
|
|
|
setSelectedKeys(prevKeys => {
|
|
|
|
|
if (checked) {
|
|
|
|
|
return [...prevKeys, personnelId];
|
|
|
|
|
} else {
|
|
|
|
|
return prevKeys.filter(id => id !== personnelId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理部门人员全选
|
|
|
|
|
* @param {string} deptId - 部门ID
|
|
|
|
|
* @param {boolean} checked - 是否全选
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const handleSelectAll = useCallback((deptId: string, checked: boolean) => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
const deptName = departments.find(d => d.id === deptId)?.name || '';
|
|
|
|
|
const deptPersonnel = personnel.filter(p =>
|
|
|
|
|
p.department === deptName &&
|
|
|
|
|
(keyword ? p.name.includes(keyword) : true)
|
|
|
|
|
);
|
|
|
|
|
const deptPersonnelIds = deptPersonnel.map(p => p.id);
|
|
|
|
|
|
2025-06-23 21:39:51 +08:00
|
|
|
|
setSelectedKeys(prevKeys => {
|
|
|
|
|
if (checked) {
|
|
|
|
|
// 添加该部门所有人员
|
|
|
|
|
const newKeys = new Set([...prevKeys, ...deptPersonnelIds]);
|
|
|
|
|
return Array.from(newKeys);
|
|
|
|
|
} else {
|
|
|
|
|
// 移除该部门所有人员
|
|
|
|
|
return prevKeys.filter(id => !deptPersonnelIds.includes(id));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, [departments, keyword, personnel]);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 确认选择
|
|
|
|
|
* 将选中的人员ID列表转换为人员对象列表
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const handleConfirm = useCallback(() => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
const selectedItems = personnel.filter(item => selectedKeys.includes(item.id));
|
|
|
|
|
onSelect(selectedItems);
|
2025-06-23 21:39:51 +08:00
|
|
|
|
}, [onSelect, personnel, selectedKeys]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算各部门已选人数
|
|
|
|
|
* @param {string} deptId - 部门ID
|
|
|
|
|
* @returns {number} - 已选人数
|
|
|
|
|
*/
|
|
|
|
|
const getSelectedCountByDept = useCallback((deptId: string) => {
|
|
|
|
|
const deptName = departments.find(d => d.id === deptId)?.name || '';
|
|
|
|
|
const deptPersonnel = personnel.filter(p => p.department === deptName);
|
|
|
|
|
const selectedCount = deptPersonnel.filter(p => selectedKeys.includes(p.id)).length;
|
|
|
|
|
return selectedCount;
|
|
|
|
|
}, [departments, personnel, selectedKeys]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 渲染Tab标签
|
|
|
|
|
* @param {Department} dept - 部门信息
|
|
|
|
|
* @returns {React.ReactNode} - 渲染结果
|
|
|
|
|
*/
|
|
|
|
|
const renderTabTitle = useCallback((dept: Department) => {
|
|
|
|
|
const selectedCount = getSelectedCountByDept(dept.id);
|
|
|
|
|
return (
|
|
|
|
|
<Badge
|
|
|
|
|
count={selectedCount > 0 ? selectedCount : 0}
|
|
|
|
|
size="small"
|
|
|
|
|
offset={[5, -3]}
|
|
|
|
|
style={{ backgroundColor: selectedCount > 0 ? '#1890ff' : '#d9d9d9' }}
|
|
|
|
|
>
|
|
|
|
|
<span>{dept.name}</span>
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
}, [getSelectedCountByDept]);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 渲染部门下的人员列表
|
|
|
|
|
* @param {string} deptId - 部门ID
|
|
|
|
|
* @returns {React.ReactNode} - 渲染结果
|
|
|
|
|
*/
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const renderDepartmentPersonnel = useCallback((deptId: string) => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
// 获取部门名称
|
|
|
|
|
const deptName = departments.find(d => d.id === deptId)?.name || '';
|
|
|
|
|
|
|
|
|
|
// 过滤出当前部门且符合搜索条件的人员
|
|
|
|
|
const deptPersonnel = personnel.filter(p =>
|
|
|
|
|
p.department === deptName &&
|
|
|
|
|
(keyword ? p.name.includes(keyword) : true)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 检查该部门是否全部选中
|
|
|
|
|
const deptPersonnelIds = deptPersonnel.map(p => p.id);
|
|
|
|
|
const isAllSelected = deptPersonnelIds.length > 0 && deptPersonnelIds.every(id => selectedKeys.includes(id));
|
|
|
|
|
|
|
|
|
|
// 如果没有符合条件的人员,显示空状态
|
|
|
|
|
if (deptPersonnel.length === 0) {
|
|
|
|
|
return <Empty description="暂无符合条件的人员" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="department-personnel">
|
|
|
|
|
<Card className="personnel-card">
|
|
|
|
|
<div className="department-header">
|
|
|
|
|
<Space>
|
|
|
|
|
<TeamOutlined />
|
|
|
|
|
<span className="department-title">{deptName}</span>
|
|
|
|
|
<Divider type="vertical" />
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={isAllSelected}
|
|
|
|
|
onChange={(e) => handleSelectAll(deptId, e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
全选
|
|
|
|
|
</Checkbox>
|
|
|
|
|
<span className="department-count">
|
|
|
|
|
(共{deptPersonnel.length}人)
|
|
|
|
|
</span>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Divider style={{ margin: '12px 0' }} />
|
|
|
|
|
|
|
|
|
|
<Row gutter={[16, 16]} className="personnel-list">
|
|
|
|
|
{deptPersonnel.map(person => (
|
|
|
|
|
<Col span={8} key={person.id}>
|
|
|
|
|
<Card
|
|
|
|
|
size="small"
|
|
|
|
|
className={`personnel-item ${selectedKeys.includes(person.id) ? 'selected' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={selectedKeys.includes(person.id)}
|
|
|
|
|
onChange={(e) => handleSelect(person.id, e.target.checked)}
|
|
|
|
|
>
|
|
|
|
|
<Space>
|
|
|
|
|
<Avatar
|
|
|
|
|
icon={<UserOutlined />}
|
|
|
|
|
size="small"
|
|
|
|
|
className={selectedKeys.includes(person.id) ? 'selected-avatar' : ''}
|
|
|
|
|
/>
|
|
|
|
|
<div className="personnel-info">
|
|
|
|
|
<div className="personnel-name">
|
|
|
|
|
{person.name}
|
|
|
|
|
{selectedKeys.includes(person.id) && (
|
|
|
|
|
<CheckCircleOutlined className="selected-icon" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="personnel-position">{person.position}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Space>
|
|
|
|
|
</Checkbox>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
))}
|
|
|
|
|
</Row>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-06-23 21:39:51 +08:00
|
|
|
|
}, [departments, handleSelect, handleSelectAll, keyword, personnel, selectedKeys]);
|
|
|
|
|
|
|
|
|
|
// 使用useMemo优化部门Tab的渲染
|
|
|
|
|
const departmentTabs = useMemo(() => {
|
|
|
|
|
return departments.map(dept => (
|
|
|
|
|
<TabPane
|
|
|
|
|
tab={renderTabTitle(dept)}
|
|
|
|
|
key={dept.id}
|
2025-06-23 19:15:13 +08:00
|
|
|
|
>
|
2025-06-23 21:39:51 +08:00
|
|
|
|
{renderDepartmentPersonnel(dept.id)}
|
|
|
|
|
</TabPane>
|
|
|
|
|
));
|
|
|
|
|
}, [departments, renderDepartmentPersonnel, renderTabTitle]);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="evaluate-task-personnel-selector">
|
|
|
|
|
{/* 顶部搜索和统计区域 */}
|
|
|
|
|
<div className="selector-header">
|
|
|
|
|
<Search
|
|
|
|
|
placeholder="请输入人员姓名搜索"
|
|
|
|
|
allowClear
|
|
|
|
|
enterButton={<><SearchOutlined /> 搜索</>}
|
|
|
|
|
onSearch={handleSearch}
|
|
|
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
|
|
|
className="search-input"
|
|
|
|
|
/>
|
|
|
|
|
<div className="selected-count">
|
|
|
|
|
<Badge
|
|
|
|
|
count={selectedKeys.length}
|
|
|
|
|
showZero
|
|
|
|
|
overflowCount={99}
|
|
|
|
|
style={{ backgroundColor: '#1890ff' }}
|
|
|
|
|
>
|
|
|
|
|
<span className="count-label">已选人员</span>
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 显示已选人员数量的提示 */}
|
|
|
|
|
{selectedKeys.length > 0 && (
|
|
|
|
|
<Alert
|
|
|
|
|
message={`已选择 ${selectedKeys.length} 名评价人员`}
|
|
|
|
|
type="info"
|
|
|
|
|
showIcon
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 部门分类标签页 */}
|
|
|
|
|
<Tabs
|
|
|
|
|
activeKey={activeTab}
|
|
|
|
|
onChange={handleTabChange}
|
|
|
|
|
className="department-tabs"
|
|
|
|
|
>
|
2025-06-23 21:39:51 +08:00
|
|
|
|
{departmentTabs}
|
2025-06-23 19:15:13 +08:00
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
{/* 底部确认按钮区域 */}
|
|
|
|
|
<div className="selector-footer">
|
|
|
|
|
<Button type="primary" onClick={handleConfirm} size="large">
|
|
|
|
|
确定选择 ({selectedKeys.length})
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default EvaluateTaskPersonnelSelector;
|