530 lines
18 KiB
TypeScript
530 lines
18 KiB
TypeScript
|
|
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
|
|||
|
|
import { Button, DatePicker, Empty, Input, Modal, Spin, Table, Tree, message } from 'antd';
|
|||
|
|
import type { ColumnsType } from 'antd/es/table';
|
|||
|
|
import { UserOutlined } from '@ant-design/icons';
|
|||
|
|
import zhCN from 'antd/es/date-picker/locale/zh_CN';
|
|||
|
|
import dayjs from 'dayjs';
|
|||
|
|
import type { Dayjs } from 'dayjs';
|
|||
|
|
import { useNavigate } from 'react-router-dom';
|
|||
|
|
import PageBackButton from '@/components/PageBackButton';
|
|||
|
|
import { getProjectExecutionInfo } from '@/api/projectExecution';
|
|||
|
|
import { deptTreeSelect, getUserProfile, listUser } from '@/api/system/user';
|
|||
|
|
import './user-project.css';
|
|||
|
|
|
|||
|
|
const { RangePicker } = DatePicker;
|
|||
|
|
|
|||
|
|
interface RoleRow {
|
|||
|
|
roleName?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface UserRow {
|
|||
|
|
_rowKey?: string;
|
|||
|
|
userId?: string | number;
|
|||
|
|
nickName?: string;
|
|||
|
|
userName?: string;
|
|||
|
|
phonenumber?: string;
|
|||
|
|
dept?: { deptName?: string };
|
|||
|
|
roles?: RoleRow[];
|
|||
|
|
[key: string]: unknown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ProjectExecutionRow {
|
|||
|
|
projectId?: string | number;
|
|||
|
|
projectName?: string;
|
|||
|
|
allWorkTime?: string | number;
|
|||
|
|
detailList?: Array<string | number | null | undefined>;
|
|||
|
|
[key: string]: unknown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
interface DeptTreeNode {
|
|||
|
|
key: string;
|
|||
|
|
title: string;
|
|||
|
|
rawId: string | number;
|
|||
|
|
children?: DeptTreeNode[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|||
|
|
typeof value === 'object' && value !== null;
|
|||
|
|
|
|||
|
|
const normalizeResponseData = (response: unknown) =>
|
|||
|
|
isObject(response) && response.data !== undefined ? response.data : response;
|
|||
|
|
|
|||
|
|
const toNumber = (value: unknown, fallback = 0) => {
|
|||
|
|
const num = Number(value);
|
|||
|
|
return Number.isFinite(num) ? num : fallback;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getDefaultRange = (): [Dayjs, Dayjs] => [dayjs().startOf('month'), dayjs().endOf('month')];
|
|||
|
|
|
|||
|
|
const normalizeExecutionRows = (payload: unknown): ProjectExecutionRow[] => {
|
|||
|
|
const data = normalizeResponseData(payload);
|
|||
|
|
const rows = Array.isArray(data)
|
|||
|
|
? data
|
|||
|
|
: isObject(data) && Array.isArray(data.rows)
|
|||
|
|
? data.rows
|
|||
|
|
: [];
|
|||
|
|
return rows.filter(isObject) as ProjectExecutionRow[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const normalizeUserRows = (payload: unknown): { rows: UserRow[]; total: number } => {
|
|||
|
|
const data = normalizeResponseData(payload);
|
|||
|
|
const rows = Array.isArray(data)
|
|||
|
|
? data
|
|||
|
|
: isObject(data) && Array.isArray(data.rows)
|
|||
|
|
? data.rows
|
|||
|
|
: [];
|
|||
|
|
const total = isObject(data) ? toNumber(data.total, rows.length) : rows.length;
|
|||
|
|
return {
|
|||
|
|
rows: rows.filter(isObject).map((row, index) => ({
|
|||
|
|
...(row as UserRow),
|
|||
|
|
_rowKey: String((row as UserRow).userId ?? (row as UserRow).userName ?? `user_${index}`),
|
|||
|
|
})) as UserRow[],
|
|||
|
|
total,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const normalizeDeptTreeNodes = (payload: unknown): DeptTreeNode[] => {
|
|||
|
|
const data = normalizeResponseData(payload);
|
|||
|
|
if (!Array.isArray(data)) {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mapNodes = (nodes: unknown[]): DeptTreeNode[] =>
|
|||
|
|
nodes.flatMap((node, index) => {
|
|||
|
|
if (!isObject(node)) {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
const rawId = node.id ?? node.deptId ?? node.value ?? `dept_${index}`;
|
|||
|
|
const normalizedId = typeof rawId === 'string' || typeof rawId === 'number' ? rawId : String(rawId);
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
key: String(normalizedId),
|
|||
|
|
rawId: normalizedId,
|
|||
|
|
title: String(node.label ?? node.title ?? node.deptName ?? normalizedId),
|
|||
|
|
children: Array.isArray(node.children) ? mapNodes(node.children) : undefined,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return mapNodes(data);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const collectDeptKeys = (nodes: DeptTreeNode[]): string[] =>
|
|||
|
|
nodes.flatMap((node) => [node.key, ...(Array.isArray(node.children) ? collectDeptKeys(node.children) : [])]);
|
|||
|
|
|
|||
|
|
const getUserDisplayName = (user?: UserRow | null) => String(user?.nickName ?? user?.userName ?? '-');
|
|||
|
|
|
|||
|
|
const matchesUserKeyword = (user: UserRow, keyword: string) => {
|
|||
|
|
const normalizedKeyword = keyword.trim();
|
|||
|
|
if (!normalizedKeyword) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return [user.nickName, user.userName, user.phonenumber].some((field) =>
|
|||
|
|
String(field ?? '')
|
|||
|
|
.toLowerCase()
|
|||
|
|
.includes(normalizedKeyword.toLowerCase()),
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const UserProjectPage = () => {
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [userLoading, setUserLoading] = useState(false);
|
|||
|
|
const [deptLoading, setDeptLoading] = useState(false);
|
|||
|
|
const [userModalOpen, setUserModalOpen] = useState(false);
|
|||
|
|
const [userKeyword, setUserKeyword] = useState('');
|
|||
|
|
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultRange());
|
|||
|
|
const [selectedUser, setSelectedUser] = useState<UserRow | null>(null);
|
|||
|
|
const [selectedUserId, setSelectedUserId] = useState<string | number>('');
|
|||
|
|
const [selectedUserName, setSelectedUserName] = useState('');
|
|||
|
|
const [pendingUserId, setPendingUserId] = useState<string | number>('');
|
|||
|
|
const [executionData, setExecutionData] = useState<ProjectExecutionRow[]>([]);
|
|||
|
|
const [userListData, setUserListData] = useState<UserRow[]>([]);
|
|||
|
|
const [deptTree, setDeptTree] = useState<DeptTreeNode[]>([]);
|
|||
|
|
const [expandedDeptKeys, setExpandedDeptKeys] = useState<string[]>([]);
|
|||
|
|
const [selectedDeptId, setSelectedDeptId] = useState<string>('');
|
|||
|
|
const [userPageNum, setUserPageNum] = useState(1);
|
|||
|
|
const [userPageSize, setUserPageSize] = useState(10);
|
|||
|
|
const [userTotal, setUserTotal] = useState(0);
|
|||
|
|
const deferredUserKeyword = useDeferredValue(userKeyword);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchCurrentUser = async () => {
|
|||
|
|
try {
|
|||
|
|
const response = await getUserProfile();
|
|||
|
|
const payload = normalizeResponseData(response);
|
|||
|
|
const user = isObject(payload) && isObject(payload.user) ? payload.user : payload;
|
|||
|
|
if (!isObject(user)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const nextUser: UserRow = {
|
|||
|
|
userId: user.userId as string | number | undefined,
|
|||
|
|
nickName: user.nickName as string | undefined,
|
|||
|
|
userName: user.userName as string | undefined,
|
|||
|
|
};
|
|||
|
|
setSelectedUser(nextUser);
|
|||
|
|
setSelectedUserId(nextUser.userId ?? '');
|
|||
|
|
setPendingUserId(nextUser.userId ?? '');
|
|||
|
|
setSelectedUserName(getUserDisplayName(nextUser));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch current user profile for user project page:', error);
|
|||
|
|
message.error('获取当前用户失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
void fetchCurrentUser();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchUserProject = async () => {
|
|||
|
|
if (!selectedUserId || !dateRange[0] || !dateRange[1]) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const response = await getProjectExecutionInfo({
|
|||
|
|
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
|||
|
|
endDate: `${dateRange[1].format('YYYY-MM-DD')} 00:00:00`,
|
|||
|
|
userId: selectedUserId,
|
|||
|
|
});
|
|||
|
|
setExecutionData(normalizeExecutionRows(response));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch user project table:', error);
|
|||
|
|
message.error('获取人员项目表失败');
|
|||
|
|
setExecutionData([]);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
void fetchUserProject();
|
|||
|
|
}, [dateRange, selectedUserId]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!userModalOpen) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const fetchDeptTree = async () => {
|
|||
|
|
setDeptLoading(true);
|
|||
|
|
try {
|
|||
|
|
const response = await deptTreeSelect();
|
|||
|
|
const treeNodes = normalizeDeptTreeNodes(response);
|
|||
|
|
setDeptTree(treeNodes);
|
|||
|
|
setExpandedDeptKeys(collectDeptKeys(treeNodes));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch department tree for user project page:', error);
|
|||
|
|
setDeptTree([]);
|
|||
|
|
setExpandedDeptKeys([]);
|
|||
|
|
} finally {
|
|||
|
|
setDeptLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
void fetchDeptTree();
|
|||
|
|
}, [userModalOpen]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!userModalOpen) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const fetchUserList = async () => {
|
|||
|
|
setUserLoading(true);
|
|||
|
|
try {
|
|||
|
|
const normalizedKeyword = deferredUserKeyword.trim();
|
|||
|
|
const response = await listUser({
|
|||
|
|
pageNum: userPageNum,
|
|||
|
|
pageSize: normalizedKeyword ? 1000 : userPageSize,
|
|||
|
|
deptId: selectedDeptId || undefined,
|
|||
|
|
});
|
|||
|
|
const { rows, total } = normalizeUserRows(response);
|
|||
|
|
const filteredRows = normalizedKeyword ? rows.filter((item) => matchesUserKeyword(item, normalizedKeyword)) : rows;
|
|||
|
|
setUserListData(filteredRows);
|
|||
|
|
setUserTotal(normalizedKeyword ? filteredRows.length : total);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch user list for user project page:', error);
|
|||
|
|
message.error('获取用户列表失败');
|
|||
|
|
setUserListData([]);
|
|||
|
|
setUserTotal(0);
|
|||
|
|
} finally {
|
|||
|
|
setUserLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
void fetchUserList();
|
|||
|
|
}, [deferredUserKeyword, selectedDeptId, userModalOpen, userPageNum, userPageSize]);
|
|||
|
|
|
|||
|
|
const pendingUser = useMemo(
|
|||
|
|
() => userListData.find((item) => String(item.userId ?? '') === String(pendingUserId ?? '')) ?? selectedUser,
|
|||
|
|
[pendingUserId, selectedUser, userListData],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
|
|||
|
|
const openUserModal = () => {
|
|||
|
|
setPendingUserId(selectedUserId);
|
|||
|
|
setUserPageNum(1);
|
|||
|
|
setSelectedDeptId('');
|
|||
|
|
setUserModalOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const applySelectedUser = (user?: UserRow | null) => {
|
|||
|
|
setSelectedUser(user ?? null);
|
|||
|
|
setSelectedUserId(user?.userId ?? '');
|
|||
|
|
setSelectedUserName(getUserDisplayName(user));
|
|||
|
|
setPendingUserId(user?.userId ?? '');
|
|||
|
|
setUserModalOpen(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const dynamicColumns = useMemo<ColumnsType<ProjectExecutionRow>>(() => {
|
|||
|
|
const columns: ColumnsType<ProjectExecutionRow> = [
|
|||
|
|
{
|
|||
|
|
title: '项目',
|
|||
|
|
dataIndex: 'projectName',
|
|||
|
|
key: 'projectName',
|
|||
|
|
fixed: 'left',
|
|||
|
|
width: 180,
|
|||
|
|
render: (value: unknown, row) => (
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
className="user-project-link"
|
|||
|
|
onClick={() => navigate(`/project/detail?id=${String(row.projectId ?? '')}`)}
|
|||
|
|
>
|
|||
|
|
{String(value ?? '-')}
|
|||
|
|
</Button>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '统计工时\n(天)',
|
|||
|
|
dataIndex: 'allWorkTime',
|
|||
|
|
key: 'allWorkTime',
|
|||
|
|
fixed: 'left',
|
|||
|
|
width: 140,
|
|||
|
|
render: (value: unknown) => toNumber(value, 0),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let cursor = dateRange[0].startOf('day');
|
|||
|
|
let index = 0;
|
|||
|
|
while (!cursor.isAfter(dateRange[1], 'day')) {
|
|||
|
|
const columnIndex = index;
|
|||
|
|
const key = cursor.format('M/D');
|
|||
|
|
const weekLabel = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][cursor.day()];
|
|||
|
|
columns.push({
|
|||
|
|
title: <span className="user-project-header">{`${weekLabel}\n${key}`}</span>,
|
|||
|
|
key: `detail-${key}`,
|
|||
|
|
dataIndex: 'detailList',
|
|||
|
|
width: 100,
|
|||
|
|
render: (value: unknown) => {
|
|||
|
|
const detailList = Array.isArray(value) ? value : [];
|
|||
|
|
return detailList[columnIndex] ?? '';
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
cursor = cursor.add(1, 'day');
|
|||
|
|
index += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return columns;
|
|||
|
|
}, [dateRange, navigate]);
|
|||
|
|
|
|||
|
|
const summaryValues = useMemo(() => {
|
|||
|
|
const dayCount = dateRange[1].startOf('day').diff(dateRange[0].startOf('day'), 'day') + 1;
|
|||
|
|
const totalWorkTime = executionData.reduce((sum, row) => sum + toNumber(row.allWorkTime, 0), 0);
|
|||
|
|
const dayTotals = Array.from({ length: Math.max(dayCount, 0) }, (_item, index) =>
|
|||
|
|
Number(
|
|||
|
|
executionData
|
|||
|
|
.reduce((sum, row) => sum + toNumber(Array.isArray(row.detailList) ? row.detailList[index] : 0, 0), 0)
|
|||
|
|
.toFixed(2),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
return { totalWorkTime: Number(totalWorkTime.toFixed(2)), dayTotals };
|
|||
|
|
}, [dateRange, executionData]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="user-project-page">
|
|||
|
|
<div className="user-project-back-row">
|
|||
|
|
<PageBackButton text="返回看板统计" fallbackPath="/projectBank/projectProgress" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="user-project-shell">
|
|||
|
|
<div className="user-project-header-row">
|
|||
|
|
<h2 className="user-project-title">人员项目表</h2>
|
|||
|
|
<div className="user-project-toolbar">
|
|||
|
|
<div className="user-project-user-box">
|
|||
|
|
<span>选择人员</span>
|
|||
|
|
<Input
|
|||
|
|
value={selectedUserName}
|
|||
|
|
placeholder="请选择用户"
|
|||
|
|
readOnly
|
|||
|
|
suffix={<UserOutlined onClick={openUserModal} />}
|
|||
|
|
onClick={openUserModal}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="user-project-range-box">
|
|||
|
|
<span>统计时间:</span>
|
|||
|
|
<RangePicker
|
|||
|
|
value={dateRange}
|
|||
|
|
onChange={(values) => {
|
|||
|
|
if (!values || values.length !== 2 || !values[0] || !values[1]) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setDateRange([values[0], values[1]]);
|
|||
|
|
}}
|
|||
|
|
allowClear={false}
|
|||
|
|
locale={zhCN}
|
|||
|
|
format="YYYY-MM-DD"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Spin spinning={loading}>
|
|||
|
|
<Table<ProjectExecutionRow>
|
|||
|
|
className="user-project-table"
|
|||
|
|
rowKey={(row) => String(row.projectId ?? row.projectName ?? '')}
|
|||
|
|
columns={dynamicColumns}
|
|||
|
|
dataSource={executionData}
|
|||
|
|
pagination={false}
|
|||
|
|
locale={{ emptyText: <Empty description="暂无项目数据" /> }}
|
|||
|
|
scroll={{ x: Math.max(dynamicColumns.length * 100, 960), y: 600 }}
|
|||
|
|
summary={() => (
|
|||
|
|
<Table.Summary.Row>
|
|||
|
|
<Table.Summary.Cell index={0}>合计工时(天)</Table.Summary.Cell>
|
|||
|
|
<Table.Summary.Cell index={1}>{summaryValues.totalWorkTime}</Table.Summary.Cell>
|
|||
|
|
{summaryValues.dayTotals.map((item, index) => (
|
|||
|
|
<Table.Summary.Cell index={index + 2} key={`sum-${index}`}>
|
|||
|
|
{item}
|
|||
|
|
</Table.Summary.Cell>
|
|||
|
|
))}
|
|||
|
|
</Table.Summary.Row>
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
</Spin>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title="选择人员"
|
|||
|
|
open={userModalOpen}
|
|||
|
|
onCancel={() => setUserModalOpen(false)}
|
|||
|
|
width={980}
|
|||
|
|
wrapClassName="user-select-dialog"
|
|||
|
|
destroyOnHidden
|
|||
|
|
footer={[
|
|||
|
|
<Button key="cancel" onClick={() => setUserModalOpen(false)}>
|
|||
|
|
取消
|
|||
|
|
</Button>,
|
|||
|
|
<Button
|
|||
|
|
key="ok"
|
|||
|
|
type="primary"
|
|||
|
|
disabled={!pendingUserId}
|
|||
|
|
onClick={() => {
|
|||
|
|
const hit =
|
|||
|
|
userListData.find((item) => String(item.userId ?? '') === String(pendingUserId ?? '')) ?? pendingUser;
|
|||
|
|
applySelectedUser(hit);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
确认
|
|||
|
|
</Button>,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<div className="user-select-modal user-select-modal-layout">
|
|||
|
|
<div className="user-select-dept-panel">
|
|||
|
|
<Spin spinning={deptLoading}>
|
|||
|
|
<Tree
|
|||
|
|
blockNode
|
|||
|
|
showLine
|
|||
|
|
className="user-select-dept-tree"
|
|||
|
|
expandedKeys={expandedDeptKeys}
|
|||
|
|
selectedKeys={selectedDeptId ? [selectedDeptId] : []}
|
|||
|
|
treeData={deptTree}
|
|||
|
|
onExpand={(keys) => setExpandedDeptKeys(keys as string[])}
|
|||
|
|
onSelect={(keys) => {
|
|||
|
|
setSelectedDeptId(String(keys[0] ?? ''));
|
|||
|
|
setUserPageNum(1);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Spin>
|
|||
|
|
</div>
|
|||
|
|
<div className="user-select-table-panel">
|
|||
|
|
<div className="user-select-query-bar">
|
|||
|
|
<Input
|
|||
|
|
placeholder="请输入姓名或手机号"
|
|||
|
|
value={userKeyword}
|
|||
|
|
onChange={(event) => {
|
|||
|
|
const nextValue = event.target.value;
|
|||
|
|
setUserPageNum(1);
|
|||
|
|
setUserKeyword(nextValue);
|
|||
|
|
}}
|
|||
|
|
allowClear
|
|||
|
|
size="large"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<Table<UserRow>
|
|||
|
|
rowKey={(row) => String(row._rowKey ?? row.userId ?? row.userName ?? '')}
|
|||
|
|
loading={userLoading}
|
|||
|
|
dataSource={userListData}
|
|||
|
|
size="middle"
|
|||
|
|
scroll={{ y: 420 }}
|
|||
|
|
rowClassName={(record) =>
|
|||
|
|
String(record.userId ?? '') === String(pendingUserId ?? '') ? 'user-select-row is-active' : 'user-select-row'
|
|||
|
|
}
|
|||
|
|
onRow={(record) => ({
|
|||
|
|
onClick: () => {
|
|||
|
|
setPendingUserId(record.userId ?? '');
|
|||
|
|
},
|
|||
|
|
onDoubleClick: () => {
|
|||
|
|
applySelectedUser(record);
|
|||
|
|
},
|
|||
|
|
})}
|
|||
|
|
locale={{ emptyText: <Empty description="暂无人员数据" /> }}
|
|||
|
|
pagination={{
|
|||
|
|
current: userPageNum,
|
|||
|
|
pageSize: userPageSize,
|
|||
|
|
total: userTotal,
|
|||
|
|
showSizeChanger: true,
|
|||
|
|
showTotal: (total) => `共 ${total} 条`,
|
|||
|
|
onChange: (page, pageSize) => {
|
|||
|
|
setUserPageNum(page);
|
|||
|
|
setUserPageSize(pageSize);
|
|||
|
|
},
|
|||
|
|
}}
|
|||
|
|
columns={[
|
|||
|
|
{
|
|||
|
|
title: '序号',
|
|||
|
|
key: 'index',
|
|||
|
|
width: 90,
|
|||
|
|
render: (_value, _row, index) => (userPageNum - 1) * userPageSize + index + 1,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '姓名',
|
|||
|
|
dataIndex: 'nickName',
|
|||
|
|
key: 'nickName',
|
|||
|
|
width: 180,
|
|||
|
|
render: (_value, row) => (
|
|||
|
|
<div className="user-select-name-cell">
|
|||
|
|
<strong>{getUserDisplayName(row)}</strong>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '部门',
|
|||
|
|
dataIndex: ['dept', 'deptName'],
|
|||
|
|
key: 'deptName',
|
|||
|
|
width: 220,
|
|||
|
|
render: (value: unknown) => String(value ?? '-'),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '角色',
|
|||
|
|
dataIndex: 'roles',
|
|||
|
|
key: 'roles',
|
|||
|
|
render: (value: unknown) =>
|
|||
|
|
Array.isArray(value) && value.length
|
|||
|
|
? value.map((item) => String((item as RoleRow)?.roleName ?? '')).filter(Boolean).join('、')
|
|||
|
|
: '-',
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default UserProjectPage;
|