This commit is contained in:
Li1304553726 2025-04-02 10:24:12 +08:00
parent 652883d142
commit ee320de2ca
4 changed files with 481 additions and 105 deletions

View File

@ -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({
@ -36,7 +39,7 @@ export class ShareCodeService {
const existingShareCode = await db.shareCode.findUnique({ const existingShareCode = await db.shareCode.findUnique({
where: { fileId }, where: { fileId },
}); });
if (existingShareCode) { if (existingShareCode) {
// 更新现有记录,但保留原有文件名 // 更新现有记录,但保留原有文件名
await db.shareCode.update({ await db.shareCode.update({
@ -46,9 +49,7 @@ export class ShareCodeService {
expiresAt, expiresAt,
isUsed: false, isUsed: false,
// 只在没有现有文件名且提供了新文件名时才更新文件名 // 只在没有现有文件名且提供了新文件名时才更新文件名
...(fileName && !existingShareCode.fileName ...(fileName && !existingShareCode.fileName ? { fileName } : {}),
? { fileName }
: {})
}, },
}); });
} else { } else {
@ -77,7 +78,7 @@ export class ShareCodeService {
async validateAndUseCode(code: string): Promise<ShareCode | null> { async validateAndUseCode(code: string): Promise<ShareCode | null> {
try { try {
console.log(`尝试验证分享码: ${code}`); console.log(`尝试验证分享码: ${code}`);
// 查找有效的分享码 // 查找有效的分享码
const shareCode = await db.shareCode.findFirst({ const shareCode = await db.shareCode.findFirst({
where: { where: {
@ -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 },
],
}, },
}); });
@ -172,4 +170,4 @@ export class ShareCodeService {
return []; return [];
} }
} }
} }

View File

@ -4,18 +4,18 @@ import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
import { useState, useRef, useCallback } from "react"; import { useState, useRef, useCallback } from "react";
import { message, Progress, Button, Tabs, DatePicker } from "antd"; import { message, Progress, Button, Tabs, DatePicker } from "antd";
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons"; import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
import {env} from '../../../../env' import { env } from '../../../../env'
const { TabPane } = Tabs; const { TabPane } = Tabs;
export default function DeptSettingPage() { export default function DeptSettingPage() {
const [uploadedFileId, setUploadedFileId] = useState<string>(''); const [uploadedFileId, setUploadedFileId] = useState<string>('');
const [uploadedFileName, setUploadedFileName] = useState<string>(''); const [uploadedFileName, setUploadedFileName] = useState<string>('');
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({}); const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
const [uploadedFiles, setUploadedFiles] = useState<{id: string, name: string}[]>([]); const [uploadedFiles, setUploadedFiles] = useState<{ id: string, name: string }[]>([]);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [expireTime, setExpireTime] = useState<Date | null>(null); const [expireTime, setExpireTime] = useState<Date | null>(null);
const dropRef = useRef<HTMLDivElement>(null); const dropRef = useRef<HTMLDivElement>(null);
// 使用您的 useTusUpload hook // 使用您的 useTusUpload hook
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({ const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
onSuccess: (result) => { onSuccess: (result) => {
@ -28,45 +28,56 @@ export default function DeptSettingPage() {
} }
}); });
// 清除已上传文件
const handleClearFile = () => {
setUploadedFileId('');
setUploadedFileName('');
setUploadedFiles([]);
setFileNameMap({});
};
// 处理文件上传 // 处理文件上传
const handleFileSelect = async (file: File) => { const handleFileSelect = async (file: File) => {
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 // 限制:如果已有上传文件,则提示用户
if (uploadedFiles.length > 0) {
message.warning('只能上传一个文件,请先删除已上传的文件');
return;
}
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
handleFileUpload( handleFileUpload(
file, file,
async (result) => { async (result) => {
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 {
console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId); console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId);
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, { const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
fileId: result.fileId, fileId: result.fileId,
fileName: file.name fileName: file.name
}), }),
}); });
const responseText = await response.text(); const responseText = await response.text();
console.log('保存文件名响应:', response.status, responseText); console.log('保存文件名响应:', response.status, responseText);
if (!response.ok) { if (!response.ok) {
console.error('保存文件名失败:', responseText); console.error('保存文件名失败:', responseText);
message.warning('文件名保存失败,下载时可能无法显示原始文件名'); message.warning('文件名保存失败,下载时可能无法显示原始文件名');
@ -77,7 +88,7 @@ export default function DeptSettingPage() {
console.error('保存文件名请求失败:', error); console.error('保存文件名请求失败:', error);
message.warning('文件名保存失败,下载时可能无法显示原始文件名'); message.warning('文件名保存失败,下载时可能无法显示原始文件名');
} }
message.success('文件上传成功'); message.success('文件上传成功');
}, },
(error) => { (error) => {
@ -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();
@ -115,7 +151,7 @@ export default function DeptSettingPage() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDragging(false); setIsDragging(false);
handleFileSelect(e.dataTransfer.files[0]); handleFileSelect(e.dataTransfer.files[0]);
}, []); }, []);
@ -135,22 +171,22 @@ export default function DeptSettingPage() {
if (!response.ok) { if (!response.ok) {
throw new Error('文件下载失败'); throw new Error('文件下载失败');
} }
// 创建下载链接 // 创建下载链接
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
// 直接使用传入的 fileName // 直接使用传入的 fileName
link.download = fileName; link.download = fileName;
// 触发下载 // 触发下载
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
message.success('文件下载开始'); message.success('文件下载开始');
} catch (error) { } catch (error) {
console.error('下载失败:', error); console.error('下载失败:', error);
@ -162,85 +198,101 @@ export default function DeptSettingPage() {
return ( return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}> <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<h2></h2> <h2></h2>
<Tabs defaultActiveKey="upload"> <Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload"> <TabPane tab="上传分享" key="upload">
{/* 文件上传区域 */} {/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}> <div style={{ marginBottom: '40px' }}>
<h3></h3> <h3></h3>
<div
ref={dropRef} {/* 如果没有已上传文件,显示上传区域 */}
onDragEnter={handleDragEnter} {uploadedFiles.length === 0 ? (
onDragOver={handleDragOver} <div
onDragLeave={handleDragLeave} ref={dropRef}
onDrop={handleDrop} onDragEnter={handleDragEnter}
style={{ onDragOver={handleDragOver}
padding: '20px', onDragLeave={handleDragLeave}
border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`, onDrop={handleDrop}
borderRadius: '8px',
textAlign: 'center',
backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
transition: 'all 0.3s',
marginBottom: '20px'
}}
>
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
<p></p>
<p style={{ fontSize: '12px', color: '#888' }}></p>
<input
type="file"
id="file-input"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files[0];
if (file) {
handleFileSelect(file);
}
}}
disabled={isUploading}
/>
<label
htmlFor="file-input"
style={{ style={{
display: 'inline-block', padding: '20px',
padding: '8px 16px', border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`,
backgroundColor: '#1890ff', borderRadius: '8px',
color: 'white', textAlign: 'center',
borderRadius: '4px', backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
cursor: 'pointer', transition: 'all 0.3s',
marginTop: '10px' marginBottom: '20px'
}} }}
> >
<UploadOutlined /> <InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
</label> <p></p>
</div> <p style={{ fontSize: '12px', color: '#888' }}></p>
<input
type="file"
id="file-input"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
}}
disabled={isUploading}
/>
<label
htmlFor="file-input"
style={{
display: 'inline-block',
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
}}
>
<UploadOutlined />
</label>
</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 && (
<div style={{ <div style={{
border: '1px solid #f0f0f0', border: '1px solid #f0f0f0',
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',
alignItems: 'center', alignItems: 'center',
flex: 1 flex: 1
}}> }}>
<div style={{ <div style={{
width: '20px', width: '20px',
height: '20px', height: '20px',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: '#52c41a', backgroundColor: '#52c41a',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -251,9 +303,11 @@ export default function DeptSettingPage() {
</div> </div>
<span>{file.name}</span> <span>{file.name}</span>
</div> </div>
<Button <Button
type="text" type="text"
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />} icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
onClick={() => handleDeleteFile(file.id)}
title="删除此文件"
/> />
</div> </div>
))} ))}
@ -262,9 +316,9 @@ export default function DeptSettingPage() {
{isUploading && ( {isUploading && (
<div style={{ marginTop: '20px' }}> <div style={{ marginTop: '20px' }}>
<Progress <Progress
percent={Math.round(Object.values(uploadProgress)[0] || 0)} percent={Math.round(Object.values(uploadProgress)[0] || 0)}
status="active" status="active"
/> />
</div> </div>
)} )}
@ -280,7 +334,7 @@ export default function DeptSettingPage() {
{uploadedFileId && ( {uploadedFileId && (
<div style={{ marginBottom: '40px' }}> <div style={{ marginBottom: '40px' }}>
<h3></h3> <h3></h3>
<ShareCodeGenerator <ShareCodeGenerator
fileId={uploadedFileId} fileId={uploadedFileId}
onSuccess={handleShareSuccess} onSuccess={handleShareSuccess}
@ -293,7 +347,7 @@ export default function DeptSettingPage() {
<TabPane tab="下载文件" key="download"> <TabPane tab="下载文件" key="download">
<div> <div>
<h3>使</h3> <h3>使</h3>
<ShareCodeValidator <ShareCodeValidator
onValidSuccess={handleValidSuccess} onValidSuccess={handleValidSuccess}
/> />
</div> </div>

View File

@ -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,10 +448,11 @@ 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>
{/* 编辑弹窗 */} {/* 编辑弹窗 */}
<Modal <Modal
title="编辑员工信息" title="编辑员工信息"
@ -194,7 +463,7 @@ export default function StaffMessage() {
destroyOnClose={true} destroyOnClose={true}
> >
{currentEditStaff && ( {currentEditStaff && (
<StaffInfoWrite <StaffInfoWrite
staffId={currentEditStaff.id} staffId={currentEditStaff.id}
initialData={currentEditStaff} initialData={currentEditStaff}
fieldValues={currentEditStaff.fieldValues} fieldValues={currentEditStaff.fieldValues}
@ -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>
</> </>
); );
} }