staff_data/apps/web/src/app/main/staffpage/stafftable/page.tsx

562 lines
23 KiB
TypeScript
Raw Normal View History

2025-03-19 15:57:48 +08:00
'use client';
2025-03-19 21:33:26 +08:00
import { useState } from 'react';
import { AgGridReact } from '@ag-grid-community/react';
import { ColDef, ColGroupDef } from '@ag-grid-community/core';
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
2025-03-19 15:57:48 +08:00
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { areaOptions } from '@web/src/app/main/staffinformation/area-options';
import type { CascaderProps } from 'antd/es/cascader';
2025-03-25 12:25:32 +08:00
import { utils, writeFile, read } from 'xlsx';
import { Modal, Input, Button, Switch, Upload, message } from 'antd';
2025-03-19 21:33:26 +08:00
import { api } from '@nice/client';
2025-03-19 22:04:01 +08:00
import DepartmentSelect from '@web/src/components/models/department/department-select';
2025-03-20 23:09:41 +08:00
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
2025-03-25 12:25:32 +08:00
import { UploadOutlined } from '@ant-design/icons';
2025-03-19 15:57:48 +08:00
// 修改函数类型定义
2025-03-25 12:25:32 +08:00
type ExcelColumn = { header: string; key: string };
2025-03-19 15:57:48 +08:00
function getAreaName(codes: string[], level?: number): string {
const result: string[] = [];
let currentLevel: CascaderProps['options'] = areaOptions;
for (const code of codes) {
const found = currentLevel?.find(opt => opt.value === code);
if (!found) break;
result.push(String(found.label));
currentLevel = found.children || [];
if (level && result.length >= level) break; // 添加层级控制
}
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
}
2025-03-25 12:25:32 +08:00
// 修改表头提取工具函数
function extractHeaders(columns: (ColDef | ColGroupDef)[]): string[] {
const result: string[] = [];
const extractHeadersRecursive = (cols: (ColDef | ColGroupDef)[]) => {
cols.forEach(col => {
if ('children' in col && col.children) {
extractHeadersRecursive(col.children);
} else if (col.headerName) {
result.push(col.headerName);
}
});
};
extractHeadersRecursive(columns);
return result;
}
2025-03-19 15:57:48 +08:00
export default function StaffTable() {
2025-03-25 12:25:32 +08:00
const { data: staffs, isLoading, refetch } = api.staff.findMany.useQuery({
2025-03-19 15:57:48 +08:00
where: { deletedAt: null },
2025-03-19 21:33:26 +08:00
include: { // 添加关联查询
department: true
}
2025-03-12 11:45:18 +08:00
});
2025-03-19 21:33:26 +08:00
const [gridApi, setGridApi] = useState<any>(null); // 添加gridApi状态
const [fileNameVisible, setFileNameVisible] = useState(false);
const [fileName, setFileName] = useState('');
const [defaultFileName] = useState(`员工数据_${new Date().toISOString().slice(0, 10)}`);
2025-03-20 23:09:41 +08:00
const [paginationEnabled, setPaginationEnabled] = useState(true);
2025-03-25 12:25:32 +08:00
const [importVisible, setImportVisible] = useState(false);
2025-03-19 21:33:26 +08:00
const handleConfirm = async () => {
setFileNameVisible(true);
2025-03-19 22:04:01 +08:00
2025-03-19 21:33:26 +08:00
};
// 添加导出处理函数
const handleFileNameConfirm = () => {
2025-03-20 23:09:41 +08:00
setFileNameVisible(false);
if (!gridApi || typeof gridApi.getRenderedNodes !== 'function') {
console.error('Grid API 未正确初始化');
return;
}
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 修改获取节点方式(使用更可靠的 getRenderedNodes
const rowNodes = gridApi.getRenderedNodes();
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 获取所有列定义
const flattenColumns = (cols: any[]): any[] => cols.flatMap(col => col.children ? flattenColumns(col.children) : col);
const allColDefs = flattenColumns(gridApi.getColumnDefs());
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 获取数据(兼容分页状态)
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 处理数据格式
const processRowData = (node: any) => {
2025-03-19 21:33:26 +08:00
const row: Record<string, any> = {};
allColDefs.forEach((colDef: any) => {
2025-03-20 23:09:41 +08:00
if (!colDef.field || !colDef.headerName) return;
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 修改字段访问方式,支持嵌套对象
const value = colDef.field.includes('.')
? colDef.field.split('.').reduce((obj: any, key: string) => (obj || {})[key], node.data)
: node.data[colDef.field];
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
let renderedValue = value;
// 应用列格式化
if (colDef.valueFormatter) {
renderedValue = colDef.valueFormatter({ value });
}
// 处理特殊数据类型
if (colDef.cellRenderer) {
const renderResult = colDef.cellRenderer({ value });
if (typeof renderResult === 'string') {
renderedValue = renderResult;
} else if (renderResult?.props?.dangerouslySetInnerHTML?.__html) {
renderedValue = renderResult.props.dangerouslySetInnerHTML.__html.replace(/<br\s*\/?>/gi, '\n');
2025-03-19 21:33:26 +08:00
}
2025-03-20 23:09:41 +08:00
}
2025-03-19 22:04:01 +08:00
2025-03-20 23:09:41 +08:00
// 统一布尔值显示
if (typeof renderedValue === 'boolean') {
renderedValue = renderedValue ? '是' : '否';
2025-03-19 21:33:26 +08:00
}
2025-03-20 23:09:41 +08:00
// 日期字段处理
if (['hireDate', 'rankDate', 'seniority'].includes(colDef.field) && value) {
renderedValue = new Date(value).toLocaleDateString('zh-CN');
}
// 特别处理部门名称显示
if (colDef.field === 'department.name' && renderedValue === undefined) {
renderedValue = node.data.department?.name || '';
}
row[colDef.headerName] = renderedValue;
2025-03-19 21:33:26 +08:00
});
return row;
2025-03-20 23:09:41 +08:00
};
try {
// 生成工作表
const rowData = rowNodes.map(processRowData);
const ws = utils.json_to_sheet(rowData);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "员工数据");
2025-03-19 21:33:26 +08:00
2025-03-20 23:09:41 +08:00
// 生成文件名
const finalFileName = fileName || `${defaultFileName}_${paginationEnabled ? '当前页' : '全部'}`;
writeFile(wb, `${finalFileName}.xlsx`);
} catch (error) {
console.error('导出失败:', error);
}
};
const handleResetFilters = () => {
if (gridApi) {
gridApi.setFilterModel(null);
gridApi.onFilterChanged(); // 触发筛选更新
}
2025-03-19 21:33:26 +08:00
};
2025-03-19 15:57:48 +08:00
const columnDefs: (ColDef | ColGroupDef)[] = [
2025-03-19 21:33:26 +08:00
{
field: 'username',
headerName: '姓名',
minWidth: 120,
2025-03-19 15:57:48 +08:00
pinned: 'left',
// floatingFilter: true // 确保启用浮动过滤器
},
2025-03-12 11:45:18 +08:00
{
2025-03-19 15:57:48 +08:00
headerName: '个人基本信息',
children: [
2025-03-19 21:33:26 +08:00
{
field: 'idNumber',
headerName: '身份证号',
2025-03-19 15:57:48 +08:00
minWidth: 180,
},
2025-03-19 21:33:26 +08:00
{
field: 'type',
headerName: '人员类型',
2025-03-19 15:57:48 +08:00
minWidth: 120,
},
{ field: 'officerId', headerName: '警号', minWidth: 120 },
{ field: 'phoneNumber', headerName: '手机号', minWidth: 130 },
{ field: 'age', headerName: '年龄', minWidth: 80 },
2025-03-19 21:33:26 +08:00
{
field: 'sex', headerName: '性别', minWidth: 80,
2025-03-20 23:09:41 +08:00
cellRenderer: (params: any) => {
switch (params.value) {
case true:
return '男';
case false:
return '女';
default:
return '未知';
}
}
2025-03-19 21:33:26 +08:00
},
2025-03-19 15:57:48 +08:00
{ field: 'bloodType', headerName: '血型', minWidth: 80 },
2025-03-19 21:33:26 +08:00
{
field: 'birthplace',
headerName: '籍贯',
2025-03-19 15:57:48 +08:00
minWidth: 200,
valueFormatter: (params) => params.value ? getAreaName(params.value.split('/')) : '',
},
{ field: 'source', headerName: '来源', minWidth: 120 },
]
2025-03-12 11:45:18 +08:00
},
{
2025-03-19 15:57:48 +08:00
headerName: '政治信息',
children: [
{ field: 'politicalStatus', headerName: '政治面貌', minWidth: 150 },
{ field: 'partyPosition', headerName: '党内职务', minWidth: 120 }
]
2025-03-12 11:45:18 +08:00
},
{
2025-03-19 15:57:48 +08:00
headerName: '职务信息',
children: [
2025-03-19 21:33:26 +08:00
{ field: 'department.name', headerName: '所属部门', minWidth: 200 },
2025-03-19 15:57:48 +08:00
{ field: 'rank', headerName: '衔职级别', minWidth: 120 },
2025-03-19 21:33:26 +08:00
{
field: 'rankDate', headerName: '衔职时间', minWidth: 120,
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
},
2025-03-20 23:09:41 +08:00
{ field: 'proxyPosition', headerName: '代理职务', minWidth: 120 },
2025-03-25 12:25:32 +08:00
{ field: 'post', headerName: '岗位', minWidth: 120 }
2025-03-19 15:57:48 +08:00
]
2025-03-12 11:45:18 +08:00
},
{
2025-03-19 15:57:48 +08:00
headerName: '入职信息',
children: [
2025-03-19 21:33:26 +08:00
{
field: 'hireDate', headerName: '入职时间', minWidth: 120,
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
},
{
field: 'seniority', headerName: '工龄认定时间', minWidth: 140,
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
},
2025-03-19 15:57:48 +08:00
{ field: 'sourceType', headerName: '来源类型', minWidth: 120 },
2025-03-19 21:33:26 +08:00
{
field: 'isReentry', headerName: '是否二次入职', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
},
{
field: 'isExtended', headerName: '是否延期服役', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
},
{
field: 'currentPositionDate', headerName: '现岗位开始时间', minWidth: 140,
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
}
2025-03-19 15:57:48 +08:00
]
2025-03-12 11:45:18 +08:00
},
{
2025-03-19 15:57:48 +08:00
headerName: '教育背景',
children: [
{ field: 'education', headerName: '学历', minWidth: 100 },
{ field: 'educationType', headerName: '学历形式', minWidth: 120 },
2025-03-19 21:33:26 +08:00
{
field: 'isGraduated', headerName: '是否毕业', minWidth: 100,
cellRenderer: (params: any) => params.value ? '是' : '否'
},
2025-03-19 15:57:48 +08:00
{ field: 'major', headerName: '专业', minWidth: 150 },
{ field: 'foreignLang', headerName: '外语能力', minWidth: 120 }
]
2025-03-12 11:45:18 +08:00
},
{
2025-03-19 15:57:48 +08:00
headerName: '培训信息',
children: [
{ field: 'trainType', headerName: '培训类型', minWidth: 120 },
{ field: 'trainInstitute', headerName: '培训机构', minWidth: 150 },
{ field: 'trainMajor', headerName: '培训专业', minWidth: 150 },
2025-03-19 21:33:26 +08:00
{
field: 'hasTrain', headerName: '是否参加培训', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
}
2025-03-19 15:57:48 +08:00
]
},
{
headerName: '鉴定信息',
children: [
{ field: 'certRank', headerName: '鉴定等级', minWidth: 120 },
{ field: 'certWork', headerName: '鉴定工种', minWidth: 120 },
2025-03-19 21:33:26 +08:00
{
field: 'hasCert', headerName: '是否参加鉴定', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
}
2025-03-19 15:57:48 +08:00
]
},
{
headerName: '工作信息',
children: [
2025-03-19 21:33:26 +08:00
{
field: 'equipment',
2025-03-19 15:57:48 +08:00
headerName: '操作维护装备',
minWidth: 150,
cellRenderer: (params: any) => (
2025-03-19 21:33:26 +08:00
<div
2025-03-19 15:57:48 +08:00
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
2025-03-19 21:33:26 +08:00
{
field: 'projects',
2025-03-19 15:57:48 +08:00
headerName: '演训任务经历',
minWidth: 150,
cellRenderer: (params: any) => (
2025-03-19 21:33:26 +08:00
<div
2025-03-19 15:57:48 +08:00
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
// 修改剩余两个字段的cellRenderer为相同结构
2025-03-19 21:33:26 +08:00
{
field: 'awards',
2025-03-19 15:57:48 +08:00
headerName: '奖励信息',
minWidth: 150,
cellRenderer: (params: any) => (
2025-03-19 21:33:26 +08:00
<div
2025-03-19 15:57:48 +08:00
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
2025-03-19 21:33:26 +08:00
{
field: 'punishments',
2025-03-19 15:57:48 +08:00
headerName: '处分信息',
minWidth: 150,
cellRenderer: (params: any) => (
2025-03-19 21:33:26 +08:00
<div
2025-03-19 15:57:48 +08:00
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
}
]
2025-03-12 11:45:18 +08:00
}
];
2025-03-19 21:33:26 +08:00
2025-03-19 15:57:48 +08:00
const defaultColDef: ColDef = {
sortable: true,
filter: 'agSetColumnFilter',
resizable: false,
flex: 1,
minWidth: 150,
maxWidth: 600,
suppressMovable: true,
2025-03-19 21:33:26 +08:00
cellStyle: {
2025-03-19 15:57:48 +08:00
whiteSpace: 'normal',
overflowWrap: 'break-word'
},
wrapText: true,
autoHeight: true
};
2025-03-25 12:25:32 +08:00
// 修改导出模板处理函数
const handleExportTemplate = () => {
const headerNames = extractHeaders(columnDefs);
// 创建一个对象,键为列名,值为空字符串
const templateRow = headerNames.reduce((obj, header) => {
obj[header] = '';
return obj;
}, {} as Record<string, string>);
// 创建工作簿和工作表
const wb = utils.book_new();
const ws = utils.json_to_sheet([templateRow], { header: headerNames });
// 设置列宽
const colWidth = headerNames.map(() => ({ wch: 20 }));
ws['!cols'] = colWidth;
utils.book_append_sheet(wb, ws, "员工模板");
writeFile(wb, `员工数据模板_${new Date().toISOString().slice(0, 10)}.xlsx`);
};
// 添加导入API钩子
const createManyMutation = api.staff.create.useMutation({
onSuccess: () => {
message.success('员工数据导入成功');
refetch(); // 刷新表格数据
setImportVisible(false);
},
onError: (error) => {
message.error(`导入失败: ${error.message}`);
}
});
// 处理Excel导入数据
const handleImportData = (excelData: any[]) => {
// 转换Excel数据为后端接受的格式
const staffData = excelData.map(row => {
// 创建一个标准的员工对象
const staff: any = {};
// 遍历列定义匹配Excel中的数据
columnDefs.forEach(colDef => {
if ('children' in colDef && colDef.children) {
colDef.children.forEach(childCol => {
if ('field' in childCol && childCol.headerName) {
// 使用表头名称查找Excel数据
const value = row[childCol.headerName];
if (value !== undefined) {
// 处理嵌套属性 (如 department.name)
if (childCol.field.includes('.')) {
const [parent, child] = childCol.field.split('.');
// 对于department.name特殊处理
if (parent === 'department' && child === 'name') {
// 仅存储部门名称,后续可处理
staff.departmentName = value;
}
} else {
// 性别特殊处理
if (childCol.field === 'sex') {
staff[childCol.field] = value === '男' ? true : value === '女' ? false : null;
} else {
staff[childCol.field] = value;
}
}
}
}
});
} else if ('field' in colDef && colDef.headerName) {
const value = row[colDef.headerName];
if (value !== undefined) {
staff[colDef.field] = value;
}
}
});
return staff;
});
// 调用后端API保存数据
if (staffData.length > 0) {
// 逐个创建员工记录
staffData.forEach(staff => {
createManyMutation.mutate({
data: staff // 直接传递正确格式的员工数据对象
});
});
message.success(`已提交${staffData.length}条员工数据导入请求`);
} else {
message.warning('没有可导入的有效数据');
}
};
2025-03-12 11:45:18 +08:00
2025-03-19 15:57:48 +08:00
return (
<div className="ag-theme-alpine w-full h-[calc(100vh-100px)]"
style={{
width: '100%',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}}
>
2025-03-19 21:33:26 +08:00
{!isLoading && (
2025-03-20 23:09:41 +08:00
<div className="flex items-center gap-4 mb-2">
<Button
onClick={handleConfirm}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
2025-03-19 22:04:01 +08:00
>
2025-03-20 23:09:41 +08:00
Excel
</Button>
2025-03-25 12:25:32 +08:00
<Button onClick={() => setImportVisible(true)} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600">
Excel
</Button>
<Button onClick={handleExportTemplate} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
</Button>
2025-03-20 23:09:41 +08:00
<Button
onClick={handleResetFilters}
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
>
</Button>
<div className="flex items-center gap-2">
<span className="text-gray-600"></span>
<Switch
checked={paginationEnabled}
onChange={(checked) => setPaginationEnabled(checked)}
checkedChildren="开"
unCheckedChildren="关"
2025-03-19 22:04:01 +08:00
/>
</div>
2025-03-20 23:09:41 +08:00
</div>
)}
2025-03-19 21:33:26 +08:00
<Modal
title="输入文件名"
open={fileNameVisible}
onOk={handleFileNameConfirm}
onCancel={() => setFileNameVisible(false)}
okText="导出"
cancelText="取消"
>
2025-03-19 22:04:01 +08:00
<Input
2025-03-19 21:33:26 +08:00
placeholder={`默认名称: ${defaultFileName}`}
value={fileName}
onChange={(e) => setFileName(e.target.value)}
/>
</Modal>
2025-03-25 12:25:32 +08:00
<Modal
title="导入员工数据"
open={importVisible}
onCancel={() => setImportVisible(false)}
footer={null}
>
<Upload
accept=".xlsx,.xls"
beforeUpload={file => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const wb = read(e.target?.result);
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
if (data.length === 0) {
message.warning('Excel文件中没有数据');
return;
}
message.info(`读取到${data.length}条数据,正在处理...`);
handleImportData(data);
} catch (error) {
console.error('解析Excel文件失败:', error);
message.error('Excel文件格式错误请确保使用正确的模板');
}
};
reader.readAsArrayBuffer(file);
return false;
}}
>
<Button icon={<UploadOutlined />}>Excel文件</Button>
</Upload>
<div className="mt-4 text-gray-500 text-sm">
使
</div>
</Modal>
2025-03-19 15:57:48 +08:00
{isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="text-gray-600 text-xl">...</div>
</div>
) : (
<AgGridReact
2025-03-20 23:09:41 +08:00
modules={[SetFilterModule, ClientSideRowModelModule]}
2025-03-19 21:33:26 +08:00
onGridReady={(params) => setGridApi(params.api)} // 添加gridApi回调
2025-03-19 15:57:48 +08:00
rowData={staffs}
columnDefs={columnDefs}
defaultColDef={{
...defaultColDef,
filterParams: {
textCustomComparator: (_, value) => value !== '',
}
}}
enableCellTextSelection={true}
2025-03-20 23:09:41 +08:00
pagination={paginationEnabled}
2025-03-19 15:57:48 +08:00
paginationAutoPageSize={true}
cacheQuickFilter={true}
/>
)}
</div>
);
}