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

562 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
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';
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';
import { utils, writeFile, read } from 'xlsx';
import { Modal, Input, Button, Switch, Upload, message } from 'antd';
import { api } from '@nice/client';
import DepartmentSelect from '@web/src/components/models/department/department-select';
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { UploadOutlined } from '@ant-design/icons';
// 修改函数类型定义
type ExcelColumn = { header: string; key: string };
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('/');
}
// 修改表头提取工具函数
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;
}
export default function StaffTable() {
const { data: staffs, isLoading, refetch } = api.staff.findMany.useQuery({
where: { deletedAt: null },
include: { // 添加关联查询
department: true
}
});
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)}`);
const [paginationEnabled, setPaginationEnabled] = useState(true);
const [importVisible, setImportVisible] = useState(false);
const handleConfirm = async () => {
setFileNameVisible(true);
};
// 添加导出处理函数
const handleFileNameConfirm = () => {
setFileNameVisible(false);
if (!gridApi || typeof gridApi.getRenderedNodes !== 'function') {
console.error('Grid API 未正确初始化');
return;
}
// 修改获取节点方式(使用更可靠的 getRenderedNodes
const rowNodes = gridApi.getRenderedNodes();
// 获取所有列定义
const flattenColumns = (cols: any[]): any[] => cols.flatMap(col => col.children ? flattenColumns(col.children) : col);
const allColDefs = flattenColumns(gridApi.getColumnDefs());
// 获取数据(兼容分页状态)
// 处理数据格式
const processRowData = (node: any) => {
const row: Record<string, any> = {};
allColDefs.forEach((colDef: any) => {
if (!colDef.field || !colDef.headerName) return;
// 修改字段访问方式,支持嵌套对象
const value = colDef.field.includes('.')
? colDef.field.split('.').reduce((obj: any, key: string) => (obj || {})[key], node.data)
: node.data[colDef.field];
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');
}
}
// 统一布尔值显示
if (typeof renderedValue === 'boolean') {
renderedValue = renderedValue ? '是' : '否';
}
// 日期字段处理
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;
});
return row;
};
try {
// 生成工作表
const rowData = rowNodes.map(processRowData);
const ws = utils.json_to_sheet(rowData);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "员工数据");
// 生成文件名
const finalFileName = fileName || `${defaultFileName}_${paginationEnabled ? '当前页' : '全部'}`;
writeFile(wb, `${finalFileName}.xlsx`);
} catch (error) {
console.error('导出失败:', error);
}
};
const handleResetFilters = () => {
if (gridApi) {
gridApi.setFilterModel(null);
gridApi.onFilterChanged(); // 触发筛选更新
}
};
const columnDefs: (ColDef | ColGroupDef)[] = [
{
field: 'username',
headerName: '姓名',
minWidth: 120,
pinned: 'left',
// floatingFilter: true // 确保启用浮动过滤器
},
{
headerName: '个人基本信息',
children: [
{
field: 'idNumber',
headerName: '身份证号',
minWidth: 180,
},
{
field: 'type',
headerName: '人员类型',
minWidth: 120,
},
{ field: 'officerId', headerName: '警号', minWidth: 120 },
{ field: 'phoneNumber', headerName: '手机号', minWidth: 130 },
{ field: 'age', headerName: '年龄', minWidth: 80 },
{
field: 'sex', headerName: '性别', minWidth: 80,
cellRenderer: (params: any) => {
switch (params.value) {
case true:
return '男';
case false:
return '女';
default:
return '未知';
}
}
},
{ field: 'bloodType', headerName: '血型', minWidth: 80 },
{
field: 'birthplace',
headerName: '籍贯',
minWidth: 200,
valueFormatter: (params) => params.value ? getAreaName(params.value.split('/')) : '',
},
{ field: 'source', headerName: '来源', minWidth: 120 },
]
},
{
headerName: '政治信息',
children: [
{ field: 'politicalStatus', headerName: '政治面貌', minWidth: 150 },
{ field: 'partyPosition', headerName: '党内职务', minWidth: 120 }
]
},
{
headerName: '职务信息',
children: [
{ field: 'department.name', headerName: '所属部门', minWidth: 200 },
{ field: 'rank', headerName: '衔职级别', minWidth: 120 },
{
field: 'rankDate', headerName: '衔职时间', minWidth: 120,
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
},
{ field: 'proxyPosition', headerName: '代理职务', minWidth: 120 },
{ field: 'post', headerName: '岗位', minWidth: 120 }
]
},
{
headerName: '入职信息',
children: [
{
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() : ''
},
{ field: 'sourceType', headerName: '来源类型', minWidth: 120 },
{
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() : ''
}
]
},
{
headerName: '教育背景',
children: [
{ field: 'education', headerName: '学历', minWidth: 100 },
{ field: 'educationType', headerName: '学历形式', minWidth: 120 },
{
field: 'isGraduated', headerName: '是否毕业', minWidth: 100,
cellRenderer: (params: any) => params.value ? '是' : '否'
},
{ field: 'major', headerName: '专业', minWidth: 150 },
{ field: 'foreignLang', headerName: '外语能力', minWidth: 120 }
]
},
{
headerName: '培训信息',
children: [
{ field: 'trainType', headerName: '培训类型', minWidth: 120 },
{ field: 'trainInstitute', headerName: '培训机构', minWidth: 150 },
{ field: 'trainMajor', headerName: '培训专业', minWidth: 150 },
{
field: 'hasTrain', headerName: '是否参加培训', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
}
]
},
{
headerName: '鉴定信息',
children: [
{ field: 'certRank', headerName: '鉴定等级', minWidth: 120 },
{ field: 'certWork', headerName: '鉴定工种', minWidth: 120 },
{
field: 'hasCert', headerName: '是否参加鉴定', minWidth: 120,
cellRenderer: (params: any) => params.value ? '是' : '否'
}
]
},
{
headerName: '工作信息',
children: [
{
field: 'equipment',
headerName: '操作维护装备',
minWidth: 150,
cellRenderer: (params: any) => (
<div
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
{
field: 'projects',
headerName: '演训任务经历',
minWidth: 150,
cellRenderer: (params: any) => (
<div
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
// 修改剩余两个字段的cellRenderer为相同结构
{
field: 'awards',
headerName: '奖励信息',
minWidth: 150,
cellRenderer: (params: any) => (
<div
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
},
{
field: 'punishments',
headerName: '处分信息',
minWidth: 150,
cellRenderer: (params: any) => (
<div
style={{ lineHeight: '24px' }}
dangerouslySetInnerHTML={{ __html: params.value?.replace(/,/g, '<br/>') || '' }}
/>
),
autoHeight: true
}
]
}
];
const defaultColDef: ColDef = {
sortable: true,
filter: 'agSetColumnFilter',
resizable: false,
flex: 1,
minWidth: 150,
maxWidth: 600,
suppressMovable: true,
cellStyle: {
whiteSpace: 'normal',
overflowWrap: 'break-word'
},
wrapText: true,
autoHeight: true
};
// 修改导出模板处理函数
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('没有可导入的有效数据');
}
};
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)'
}}
>
{!isLoading && (
<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"
>
Excel
</Button>
<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>
<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="关"
/>
</div>
</div>
)}
<Modal
title="输入文件名"
open={fileNameVisible}
onOk={handleFileNameConfirm}
onCancel={() => setFileNameVisible(false)}
okText="导出"
cancelText="取消"
>
<Input
placeholder={`默认名称: ${defaultFileName}`}
value={fileName}
onChange={(e) => setFileName(e.target.value)}
/>
</Modal>
<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>
{isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="text-gray-600 text-xl">...</div>
</div>
) : (
<AgGridReact
modules={[SetFilterModule, ClientSideRowModelModule]}
onGridReady={(params) => setGridApi(params.api)} // 添加gridApi回调
rowData={staffs}
columnDefs={columnDefs}
defaultColDef={{
...defaultColDef,
filterParams: {
textCustomComparator: (_, value) => value !== '',
}
}}
enableCellTextSelection={true}
pagination={paginationEnabled}
paginationAutoPageSize={true}
cacheQuickFilter={true}
/>
)}
</div>
);
}