add
This commit is contained in:
parent
652883d142
commit
ee320de2ca
|
@ -11,12 +11,15 @@ export class ShareCodeService {
|
||||||
// 生成8位分享码,使用易读的字符
|
// 生成8位分享码,使用易读的字符
|
||||||
private readonly generateCode = customAlphabet(
|
private readonly generateCode = customAlphabet(
|
||||||
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||||||
8
|
8,
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(private readonly resourceService: ResourceService) {}
|
constructor(private readonly resourceService: ResourceService) {}
|
||||||
|
|
||||||
async generateShareCode(fileId: string, fileName?: string): Promise<GenerateShareCodeResponse> {
|
async generateShareCode(
|
||||||
|
fileId: string,
|
||||||
|
fileName?: string,
|
||||||
|
): Promise<GenerateShareCodeResponse> {
|
||||||
try {
|
try {
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
const resource = await this.resourceService.findUnique({
|
const resource = await this.resourceService.findUnique({
|
||||||
|
@ -46,9 +49,7 @@ export class ShareCodeService {
|
||||||
expiresAt,
|
expiresAt,
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
||||||
...(fileName && !existingShareCode.fileName
|
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
|
||||||
? { fileName }
|
|
||||||
: {})
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -117,10 +118,7 @@ export class ShareCodeService {
|
||||||
try {
|
try {
|
||||||
const result = await db.shareCode.deleteMany({
|
const result = await db.shareCode.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
|
||||||
{ expiresAt: { lt: new Date() } },
|
|
||||||
{ isUsed: true },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,22 @@ export default function DeptSettingPage() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 清除已上传文件
|
||||||
|
const handleClearFile = () => {
|
||||||
|
setUploadedFileId('');
|
||||||
|
setUploadedFileName('');
|
||||||
|
setUploadedFiles([]);
|
||||||
|
setFileNameMap({});
|
||||||
|
};
|
||||||
|
|
||||||
// 处理文件上传
|
// 处理文件上传
|
||||||
const handleFileSelect = async (file: File) => {
|
const handleFileSelect = async (file: File) => {
|
||||||
|
// 限制:如果已有上传文件,则提示用户
|
||||||
|
if (uploadedFiles.length > 0) {
|
||||||
|
message.warning('只能上传一个文件,请先删除已上传的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
||||||
|
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
|
@ -38,15 +52,13 @@ export default function DeptSettingPage() {
|
||||||
setUploadedFileId(result.fileId);
|
setUploadedFileId(result.fileId);
|
||||||
setUploadedFileName(result.fileName);
|
setUploadedFileName(result.fileName);
|
||||||
|
|
||||||
|
|
||||||
// 添加到已上传文件列表
|
// 添加到已上传文件列表
|
||||||
setUploadedFiles(prev => [...prev, {id: result.fileId, name: file.name}]);
|
setUploadedFiles([{ id: result.fileId, name: file.name }]);
|
||||||
|
|
||||||
// 在前端保存文件名映射(用于当前会话)
|
// 在前端保存文件名映射(用于当前会话)
|
||||||
setFileNameMap(prev => ({
|
setFileNameMap({
|
||||||
...prev,
|
|
||||||
[result.fileId]: file.name
|
[result.fileId]: file.name
|
||||||
}));
|
});
|
||||||
|
|
||||||
// 上传成功后保存原始文件名到数据库
|
// 上传成功后保存原始文件名到数据库
|
||||||
try {
|
try {
|
||||||
|
@ -63,7 +75,6 @@ export default function DeptSettingPage() {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
console.log('保存文件名响应:', response.status, responseText);
|
console.log('保存文件名响应:', response.status, responseText);
|
||||||
|
|
||||||
|
@ -87,11 +98,36 @@ export default function DeptSettingPage() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理多个文件上传
|
// 处理多个文件上传 - 已移除
|
||||||
// const handleFilesUpload = (file: File) => {
|
// const handleFilesUpload = (file: File) => {
|
||||||
// handleFileSelect(file);
|
// handleFileSelect(file);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
// 处理文件删除
|
||||||
|
const handleDeleteFile = async (fileId: string) => {
|
||||||
|
try {
|
||||||
|
// 可以添加删除文件的API调用
|
||||||
|
// const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, {
|
||||||
|
// method: 'DELETE'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error('删除文件失败');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 无论服务器删除是否成功,前端都需要更新状态
|
||||||
|
setUploadedFiles([]);
|
||||||
|
setUploadedFileId('');
|
||||||
|
setUploadedFileName('');
|
||||||
|
setFileNameMap({});
|
||||||
|
|
||||||
|
message.success('文件已删除');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除文件错误:', error);
|
||||||
|
message.error('删除文件失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 拖拽相关处理函数
|
// 拖拽相关处理函数
|
||||||
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -168,6 +204,9 @@ export default function DeptSettingPage() {
|
||||||
{/* 文件上传区域 */}
|
{/* 文件上传区域 */}
|
||||||
<div style={{ marginBottom: '40px' }}>
|
<div style={{ marginBottom: '40px' }}>
|
||||||
<h3>第一步:上传文件</h3>
|
<h3>第一步:上传文件</h3>
|
||||||
|
|
||||||
|
{/* 如果没有已上传文件,显示上传区域 */}
|
||||||
|
{uploadedFiles.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
ref={dropRef}
|
ref={dropRef}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
|
@ -186,20 +225,19 @@ export default function DeptSettingPage() {
|
||||||
>
|
>
|
||||||
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
|
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
|
||||||
<p>点击或拖拽文件到此区域进行上传</p>
|
<p>点击或拖拽文件到此区域进行上传</p>
|
||||||
<p style={{ fontSize: '12px', color: '#888' }}>支持单个上传文件</p>
|
<p style={{ fontSize: '12px', color: '#888' }}>只能上传单个文件</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="file-input"
|
id="file-input"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
handleFileSelect(file);
|
handleFileSelect(file);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="file-input"
|
htmlFor="file-input"
|
||||||
|
@ -216,6 +254,21 @@ export default function DeptSettingPage() {
|
||||||
<UploadOutlined /> 选择文件
|
<UploadOutlined /> 选择文件
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={{
|
||||||
|
padding: '10px',
|
||||||
|
backgroundColor: '#f6ffed',
|
||||||
|
border: '1px solid #b7eb8f',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}>
|
||||||
|
<p style={{ color: '#52c41a', margin: 0 }}>
|
||||||
|
您已上传文件,请继续下一步生成分享码
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 已上传文件列表 */}
|
{/* 已上传文件列表 */}
|
||||||
{uploadedFiles.length > 0 && (
|
{uploadedFiles.length > 0 && (
|
||||||
|
@ -224,13 +277,12 @@ export default function DeptSettingPage() {
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{uploadedFiles.map((file, index) => (
|
{uploadedFiles.map((file) => (
|
||||||
<div key={file.id} style={{
|
<div key={file.id} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '10px 15px',
|
padding: '10px 15px',
|
||||||
borderBottom: index < uploadedFiles.length - 1 ? '1px solid #f0f0f0' : 'none',
|
backgroundColor: '#fafafa'
|
||||||
backgroundColor: index % 2 === 0 ? '#fafafa' : 'white'
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -254,6 +306,8 @@ export default function DeptSettingPage() {
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
|
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
|
onClick={() => handleDeleteFile(file.id)}
|
||||||
|
title="删除此文件"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { AgGridReact } from 'ag-grid-react';
|
import { AgGridReact } from 'ag-grid-react';
|
||||||
import { api, useStaff } from "@nice/client";
|
import { api, useStaff } from "@nice/client";
|
||||||
import { Button, CascaderProps, message, Modal } from 'antd';
|
import { Button, CascaderProps, message, Modal, Input, Upload } from 'antd';
|
||||||
import { areaOptions } from '@web/src/app/main/staffinfo_write/area-options';
|
import { areaOptions } from '@web/src/app/main/staffinfo_write/area-options';
|
||||||
import StaffInfoWrite from '@web/src/app/main/staffinfo_write/staffinfo_write.page';
|
import StaffInfoWrite from '@web/src/app/main/staffinfo_write/staffinfo_write.page';
|
||||||
|
import { utils, writeFile, read } from 'xlsx';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
function getAreaName(codes: string[], level?: number): string {
|
function getAreaName(codes: string[], level?: number): string {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
@ -18,6 +20,24 @@ function getAreaName(codes: string[], level?: number): string {
|
||||||
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
|
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加表头提取工具函数
|
||||||
|
function extractHeaders(columns: any[]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
const extractHeadersRecursive = (cols: any[]) => {
|
||||||
|
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 StaffMessage() {
|
export default function StaffMessage() {
|
||||||
const [rowData, setRowData] = useState<any[]>([]);
|
const [rowData, setRowData] = useState<any[]>([]);
|
||||||
const [columnDefs, setColumnDefs] = useState<any[]>([]);
|
const [columnDefs, setColumnDefs] = useState<any[]>([]);
|
||||||
|
@ -26,6 +46,11 @@ export default function StaffMessage() {
|
||||||
const fields = useCustomFields();
|
const fields = useCustomFields();
|
||||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||||
const [currentEditStaff, setCurrentEditStaff] = useState<any>(null);
|
const [currentEditStaff, setCurrentEditStaff] = useState<any>(null);
|
||||||
|
const [gridApi, setGridApi] = useState<any>(null);
|
||||||
|
const [fileNameVisible, setFileNameVisible] = useState(false);
|
||||||
|
const [fileName, setFileName] = useState('');
|
||||||
|
const [defaultFileName] = useState(`员工数据_${new Date().toISOString().slice(0, 10)}`);
|
||||||
|
const [importVisible, setImportVisible] = useState(false);
|
||||||
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
const { data: staffData } = api.staff.findMany.useQuery({
|
const { data: staffData } = api.staff.findMany.useQuery({
|
||||||
|
@ -61,7 +86,6 @@ export default function StaffMessage() {
|
||||||
setCurrentEditStaff(selectedRows[0]);
|
setCurrentEditStaff(selectedRows[0]);
|
||||||
setIsEditModalVisible(true);
|
setIsEditModalVisible(true);
|
||||||
}, [selectedRows]);
|
}, [selectedRows]);
|
||||||
console.log('选中行',currentEditStaff);
|
|
||||||
// 处理编辑完成
|
// 处理编辑完成
|
||||||
const handleEditComplete = useCallback(() => {
|
const handleEditComplete = useCallback(() => {
|
||||||
setIsEditModalVisible(false);
|
setIsEditModalVisible(false);
|
||||||
|
@ -150,6 +174,241 @@ export default function StaffMessage() {
|
||||||
staffData && setRowData(staffData);
|
staffData && setRowData(staffData);
|
||||||
}, [staffData]);
|
}, [staffData]);
|
||||||
|
|
||||||
|
// 修改导出模板处理函数
|
||||||
|
const handleExportTemplate = useCallback(() => {
|
||||||
|
const headerNames = extractHeaders(columnDefs);
|
||||||
|
|
||||||
|
// 创建示例数据行
|
||||||
|
let exampleRow: Record<string, string> = {};
|
||||||
|
|
||||||
|
// 检查是否有选中行
|
||||||
|
if (selectedRows.length > 0) {
|
||||||
|
// 使用第一条选中的记录作为模板数据
|
||||||
|
const templateData = selectedRows[0];
|
||||||
|
|
||||||
|
// 基础字段
|
||||||
|
exampleRow['姓名'] = templateData.showname || '';
|
||||||
|
exampleRow['所属部门'] = templateData.department?.name || '';
|
||||||
|
|
||||||
|
// 处理自定义字段
|
||||||
|
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
|
||||||
|
fieldsList.forEach((field: any) => {
|
||||||
|
const fieldValue = templateData.fieldValues?.find(
|
||||||
|
(fv: any) => fv.fieldId === field.id
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
let displayValue = fieldValue;
|
||||||
|
|
||||||
|
// 根据字段类型处理值
|
||||||
|
if (field.type === 'cascader' && fieldValue) {
|
||||||
|
displayValue = getAreaName(fieldValue.split('/'));
|
||||||
|
} else if (field.type === 'date' && fieldValue) {
|
||||||
|
displayValue = new Date(fieldValue).toLocaleDateString();
|
||||||
|
} else if (field.type === 'textarea' && fieldValue) {
|
||||||
|
displayValue = fieldValue.replace(/,/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleRow[field.label || field.name] = displayValue || '';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果没有选中行,使用默认示例数据
|
||||||
|
exampleRow['姓名'] = '张三';
|
||||||
|
exampleRow['所属部门'] = '技术部';
|
||||||
|
|
||||||
|
// 添加所有自定义字段的空值
|
||||||
|
fieldsList.forEach((field: any) => {
|
||||||
|
exampleRow[field.label || field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建空白行供用户填写
|
||||||
|
const emptyRow = headerNames.reduce((obj, header) => {
|
||||||
|
obj[header] = '';
|
||||||
|
return obj;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
// 创建工作簿和工作表
|
||||||
|
const wb = utils.book_new();
|
||||||
|
const ws = utils.json_to_sheet([exampleRow], { header: headerNames });
|
||||||
|
|
||||||
|
// 设置列宽
|
||||||
|
const colWidth = headerNames.map(() => ({ wch: 20 }));
|
||||||
|
ws['!cols'] = colWidth;
|
||||||
|
|
||||||
|
// 在第二行添加提示文字
|
||||||
|
const rowIdx = 2; // 第二行索引
|
||||||
|
const cellRef = utils.encode_cell({ r: rowIdx, c: 0 }); // A3单元格
|
||||||
|
|
||||||
|
const tipText = selectedRows.length > 0
|
||||||
|
? '以上为选中人员数据,请在下方行填写实际数据'
|
||||||
|
: '以上为示例数据,请在下方行填写实际数据';
|
||||||
|
|
||||||
|
ws[cellRef] = { t: 's', v: tipText };
|
||||||
|
|
||||||
|
// 手动添加空白行
|
||||||
|
utils.sheet_add_json(ws, [emptyRow], { skipHeader: true, origin: rowIdx + 1 });
|
||||||
|
|
||||||
|
// 合并提示文字单元格
|
||||||
|
if (!ws['!merges']) ws['!merges'] = [];
|
||||||
|
ws['!merges'].push({
|
||||||
|
s: { r: rowIdx, c: 0 }, // 起始单元格 A3
|
||||||
|
e: { r: rowIdx, c: Math.min(5, headerNames.length - 1) } // 结束单元格,跨越多列
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.book_append_sheet(wb, ws, "员工模板");
|
||||||
|
writeFile(wb, `员工数据模板_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||||
|
}, [columnDefs, selectedRows, fields.data]);
|
||||||
|
|
||||||
|
// 导出数据处理函数
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
setFileNameVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理文件名确认
|
||||||
|
const handleFileNameConfirm = useCallback(() => {
|
||||||
|
setFileNameVisible(false);
|
||||||
|
if (!gridApi) {
|
||||||
|
console.error('Grid API 未正确初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取所有数据
|
||||||
|
const allData = selectedRows.length > 0 ? selectedRows : rowData;
|
||||||
|
|
||||||
|
// 格式化数据
|
||||||
|
const exportData = allData.map(row => {
|
||||||
|
const formattedRow: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 基础字段
|
||||||
|
formattedRow['姓名'] = row.showname || '';
|
||||||
|
formattedRow['所属部门'] = row.department?.name || '';
|
||||||
|
|
||||||
|
// 动态字段
|
||||||
|
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
|
||||||
|
fieldsList.forEach((field: any) => {
|
||||||
|
const fieldValue = row.fieldValues?.find((fv: any) => fv.fieldId === field.id)?.value;
|
||||||
|
let displayValue = fieldValue;
|
||||||
|
|
||||||
|
// 根据字段类型处理值
|
||||||
|
if (field.type === 'cascader' && fieldValue) {
|
||||||
|
displayValue = getAreaName(fieldValue.split('/'));
|
||||||
|
} else if (field.type === 'date' && fieldValue) {
|
||||||
|
displayValue = new Date(fieldValue).toLocaleDateString();
|
||||||
|
} else if (field.type === 'textarea' && fieldValue) {
|
||||||
|
displayValue = fieldValue.replace(/,/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedRow[field.label || field.name] = displayValue || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成工作表
|
||||||
|
const ws = utils.json_to_sheet(exportData);
|
||||||
|
const wb = utils.book_new();
|
||||||
|
utils.book_append_sheet(wb, ws, "员工数据");
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
const finalFileName = fileName || defaultFileName;
|
||||||
|
writeFile(wb, `${finalFileName}.xlsx`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出失败:', error);
|
||||||
|
message.error('导出失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}, [fileName, defaultFileName, rowData, selectedRows, fields.data, gridApi]);
|
||||||
|
|
||||||
|
// 处理导入数据
|
||||||
|
const createMany = api.staff.create.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('员工数据导入成功');
|
||||||
|
// 刷新数据
|
||||||
|
api.staff.findMany.useQuery();
|
||||||
|
setImportVisible(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
message.error(`导入失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理Excel导入数据
|
||||||
|
const handleImportData = useCallback((excelData: any[]) => {
|
||||||
|
if (excelData.length === 0) {
|
||||||
|
message.warning('没有可导入的数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将Excel数据转换为API需要的格式
|
||||||
|
const staffData = excelData.map(row => {
|
||||||
|
const staff: any = { fieldValues: [] };
|
||||||
|
|
||||||
|
// 处理基础字段
|
||||||
|
if (row['姓名']) staff.showname = row['姓名'];
|
||||||
|
|
||||||
|
// 处理部门
|
||||||
|
if (row['所属部门']) {
|
||||||
|
// 简单存储部门名称,后续可能需要查询匹配
|
||||||
|
staff.departmentName = row['所属部门'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自定义字段
|
||||||
|
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
|
||||||
|
fieldsList.forEach((field: any) => {
|
||||||
|
let value = row[field.label || field.name];
|
||||||
|
|
||||||
|
// 跳过空值
|
||||||
|
if (value === undefined || value === '') return;
|
||||||
|
|
||||||
|
// 根据字段类型处理输入值
|
||||||
|
switch (field.type) {
|
||||||
|
case 'cascader':
|
||||||
|
// 级联选择器可能需要将显示名称转回代码
|
||||||
|
// 这里简单保留原值,实际可能需要查询转换
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
// 尝试将日期字符串转换为ISO格式
|
||||||
|
try {
|
||||||
|
const dateObj = new Date(value);
|
||||||
|
if (!isNaN(dateObj.getTime())) {
|
||||||
|
value = dateObj.toISOString();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`日期格式转换错误: ${value}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
// 将换行符替换回逗号进行存储
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.replace(/\n/g, ',');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 可以根据需要添加其他字段类型的处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到fieldValues数组
|
||||||
|
staff.fieldValues.push({
|
||||||
|
fieldId: field.id,
|
||||||
|
value: String(value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return staff;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提交数据
|
||||||
|
createMany.mutate({ data: staffData });
|
||||||
|
message.info(`正在导入${staffData.length}条员工数据...`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理导入数据失败:', error);
|
||||||
|
message.error('数据格式错误,导入失败');
|
||||||
|
}
|
||||||
|
}, [fields.data, createMany]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
@ -158,6 +417,15 @@ export default function StaffMessage() {
|
||||||
onClick={handleDelete}>批量删除</Button>
|
onClick={handleDelete}>批量删除</Button>
|
||||||
<Button className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
|
<Button className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
|
||||||
onClick={handleEdit}>编辑</Button>
|
onClick={handleEdit}>编辑</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-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||||
|
导出模板
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
||||||
|
导出所有数据
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
|
<Button className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -180,6 +448,7 @@ export default function StaffMessage() {
|
||||||
headerHeight={40}
|
headerHeight={40}
|
||||||
rowHeight={40}
|
rowHeight={40}
|
||||||
domLayout="autoHeight"
|
domLayout="autoHeight"
|
||||||
|
onGridReady={(params) => setGridApi(params.api)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,6 +472,61 @@ export default function StaffMessage() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 导出文件名对话框 */}
|
||||||
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Reference in New Issue