add
This commit is contained in:
parent
fbbc919ca4
commit
45d2f36b37
|
@ -114,26 +114,31 @@ const TestPage: React.FC = () => {
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败:' + error.message);
|
||||
message.error('操作失败:' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
refetch();
|
||||
message.success('数据已更新');
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="h-screen w-full p-6"> {/* 修改这里,使用 h-screen 而不是 h-full */}
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">培训情况记录</h1>
|
||||
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onSearchTextChange={setSearchText}
|
||||
onSearch={handleSearch}
|
||||
onAdd={handleAdd}
|
||||
onImportSuccess={handleImportSuccess}
|
||||
data={data?.items || []}
|
||||
/>
|
||||
|
||||
<StaffTable
|
||||
data={data?.items || []}
|
||||
total={data?.total || 0}
|
||||
|
@ -144,7 +149,6 @@ const TestPage: React.FC = () => {
|
|||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<StaffModal
|
||||
visible={isModalVisible}
|
||||
editingStaff={editingStaff}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import { Button, Upload, message } from 'antd';
|
||||
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { Staff } from './types';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface ImportExportButtonsProps {
|
||||
onImportSuccess: () => void;
|
||||
data: Staff[];
|
||||
}
|
||||
|
||||
export const ImportExportButtons: React.FC<ImportExportButtonsProps> = ({
|
||||
onImportSuccess,
|
||||
data,
|
||||
}) => {
|
||||
const createMutation = api.staff.create.useMutation({
|
||||
onSuccess: () => {
|
||||
// 成功时不显示单条消息,让最终统计来显示
|
||||
},
|
||||
onError: (error) => {
|
||||
// 只有当不是用户名重复错误时才显示错误信息
|
||||
if (!error.message.includes('Unique constraint failed')) {
|
||||
message.error('导入失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加恢复记录的 mutation
|
||||
const restoreMutation = api.staff.update.useMutation({
|
||||
onSuccess: () => {
|
||||
// 静默成功,不显示消息
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('恢复记录失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const workbook = XLSX.read(e.target?.result, { type: 'binary' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet);
|
||||
|
||||
let successCount = 0;
|
||||
let restoredCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// 转换数据格式
|
||||
const staffData = jsonData.map((row: any) => ({
|
||||
username: row['用户名'],
|
||||
showname: row['名称'],
|
||||
absent: row['是否在位'] === '在位',
|
||||
}));
|
||||
|
||||
// 批量处理
|
||||
for (const staff of staffData) {
|
||||
try {
|
||||
// 先尝试创建新记录
|
||||
await createMutation.mutateAsync({
|
||||
data: staff
|
||||
});
|
||||
successCount++;
|
||||
} catch (error: any) {
|
||||
// 如果是用户名重复错误
|
||||
if (error.message.includes('Unique constraint failed')) {
|
||||
try {
|
||||
// 尝试恢复已删除的记录
|
||||
await restoreMutation.mutateAsync({
|
||||
where: {
|
||||
username: staff.username,
|
||||
},
|
||||
data: {
|
||||
deletedAt: null,
|
||||
showname: staff.showname,
|
||||
absent: staff.absent,
|
||||
}
|
||||
});
|
||||
restoredCount++;
|
||||
} catch (restoreError) {
|
||||
errorCount++;
|
||||
console.error('恢复记录失败:', restoreError);
|
||||
}
|
||||
} else {
|
||||
errorCount++;
|
||||
console.error('创建记录失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入结果
|
||||
if (successCount > 0 || restoredCount > 0) {
|
||||
let successMessage = [];
|
||||
if (successCount > 0) {
|
||||
successMessage.push(`新增 ${successCount} 条`);
|
||||
}
|
||||
if (restoredCount > 0) {
|
||||
successMessage.push(`恢复 ${restoredCount} 条`);
|
||||
}
|
||||
message.success(`导入完成:${successMessage.join(',')}`);
|
||||
onImportSuccess();
|
||||
}
|
||||
if (errorCount > 0) {
|
||||
message.warning(`${errorCount} 条记录导入失败`);
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
} catch (error) {
|
||||
message.error('文件读取失败');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
try {
|
||||
const exportData = data.map(staff => ({
|
||||
'用户名': staff.username,
|
||||
'名称': staff.showname,
|
||||
'是否在位': staff.absent ? '是' : '否'
|
||||
}));
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '员工列表');
|
||||
|
||||
XLSX.writeFile(workbook, `员工列表_${new Date().toLocaleDateString()}.xlsx`);
|
||||
message.success('导出成功');
|
||||
} catch (error) {
|
||||
message.error('导出失败: ' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
beforeUpload={handleImport}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,12 +1,22 @@
|
|||
import { Button, Input, Space } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { ImportExportButtons } from './ImportExportButtons';
|
||||
|
||||
// Define or import the Staff type
|
||||
interface Staff {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface SearchBarProps {
|
||||
searchText: string;
|
||||
onSearchTextChange: (text: string) => void;
|
||||
onSearch: () => void;
|
||||
onAdd: () => void;
|
||||
onImportSuccess: () => void;
|
||||
data: Staff[];
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
|
@ -14,6 +24,8 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|||
onSearchTextChange,
|
||||
onSearch,
|
||||
onAdd,
|
||||
onImportSuccess,
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-4 flex justify-between">
|
||||
|
@ -27,9 +39,15 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|||
/>
|
||||
<Button type="primary" onClick={onSearch}>搜索</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<ImportExportButtons
|
||||
onImportSuccess={onImportSuccess}
|
||||
data={data}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
||||
添加员工
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -3,6 +3,7 @@ import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
|||
import React, { useState } from 'react';
|
||||
import { Staff } from './types';
|
||||
|
||||
|
||||
interface StaffTableProps {
|
||||
data: Staff[];
|
||||
total: number;
|
||||
|
@ -82,15 +83,35 @@ export const StaffTable: React.FC<StaffTableProps> = ({
|
|||
},
|
||||
];
|
||||
|
||||
// 修改样式定义
|
||||
const containerStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
const tableContainerStyle = {
|
||||
flex: 1,
|
||||
height: 'calc(100vh - 240px)', // 减去头部、搜索栏和分页的高度
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const tableStyle = {
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
<div style={tableContainerStyle}>
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
loading={isLoading}
|
||||
style={tableStyle}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center items-center">
|
||||
<div className="bg-white px-6 py-3 rounded-lg shadow-sm flex items-center space-x-6">
|
||||
<Pagination
|
||||
|
@ -105,7 +126,7 @@ export const StaffTable: React.FC<StaffTableProps> = ({
|
|||
)}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
<div className="h-6 w-px bg-gray-200" /> {/* 分隔线 */}
|
||||
<div className="h-6 w-px bg-gray-200" />
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
size="small"
|
||||
|
@ -117,8 +138,8 @@ export const StaffTable: React.FC<StaffTableProps> = ({
|
|||
className="text-center"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleJumpPage}
|
||||
>
|
||||
跳转
|
||||
|
|
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://192.168.239.194:3001/auth/file;
|
||||
proxy_pass http://192.168.239.194:3000/auth/file;
|
||||
|
||||
# 请求优化:不传递请求体
|
||||
proxy_pass_request_body off;
|
||||
|
|
17708
pnpm-lock.yaml
17708
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue