add
This commit is contained in:
parent
fbbc919ca4
commit
45d2f36b37
|
@ -114,26 +114,31 @@ const TestPage: React.FC = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('操作失败:' + error.message);
|
message.error('操作失败:' + (error as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
refetch();
|
||||||
|
message.success('数据已更新');
|
||||||
|
};
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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>
|
<h1 className="text-2xl font-bold mb-4 text-center">培训情况记录</h1>
|
||||||
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
onSearchTextChange={setSearchText}
|
onSearchTextChange={setSearchText}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onAdd={handleAdd}
|
onAdd={handleAdd}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
data={data?.items || []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StaffTable
|
<StaffTable
|
||||||
data={data?.items || []}
|
data={data?.items || []}
|
||||||
total={data?.total || 0}
|
total={data?.total || 0}
|
||||||
|
@ -144,7 +149,6 @@ const TestPage: React.FC = () => {
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StaffModal
|
<StaffModal
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
editingStaff={editingStaff}
|
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 { Button, Input, Space } from 'antd';
|
||||||
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
|
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ImportExportButtons } from './ImportExportButtons';
|
||||||
|
|
||||||
|
// Define or import the Staff type
|
||||||
|
interface Staff {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
onSearchTextChange: (text: string) => void;
|
onSearchTextChange: (text: string) => void;
|
||||||
onSearch: () => void;
|
onSearch: () => void;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
|
onImportSuccess: () => void;
|
||||||
|
data: Staff[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
|
@ -14,22 +24,30 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
onSearchTextChange,
|
onSearchTextChange,
|
||||||
onSearch,
|
onSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
|
onImportSuccess,
|
||||||
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<Space>
|
<Space>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索员工姓名或用户名"
|
placeholder="搜索员工姓名或用户名"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => onSearchTextChange(e.target.value)}
|
onChange={(e) => onSearchTextChange(e.target.value)}
|
||||||
onPressEnter={onSearch}
|
onPressEnter={onSearch}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
<Button type="primary" onClick={onSearch}>搜索</Button>
|
<Button type="primary" onClick={onSearch}>搜索</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
<Space>
|
||||||
添加员工
|
<ImportExportButtons
|
||||||
</Button>
|
onImportSuccess={onImportSuccess}
|
||||||
</div>
|
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 React, { useState } from 'react';
|
||||||
import { Staff } from './types';
|
import { Staff } from './types';
|
||||||
|
|
||||||
|
|
||||||
interface StaffTableProps {
|
interface StaffTableProps {
|
||||||
data: Staff[];
|
data: Staff[];
|
||||||
total: number;
|
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 (
|
return (
|
||||||
<div>
|
<div style={containerStyle}>
|
||||||
<Table
|
<div style={tableContainerStyle}>
|
||||||
dataSource={data}
|
<Table
|
||||||
columns={columns}
|
dataSource={data}
|
||||||
rowKey="id"
|
columns={columns}
|
||||||
pagination={false}
|
rowKey="id"
|
||||||
loading={isLoading}
|
pagination={false}
|
||||||
/>
|
loading={isLoading}
|
||||||
|
style={tableStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="mt-6 flex justify-center items-center">
|
<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">
|
<div className="bg-white px-6 py-3 rounded-lg shadow-sm flex items-center space-x-6">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
@ -105,7 +126,7 @@ export const StaffTable: React.FC<StaffTableProps> = ({
|
||||||
)}
|
)}
|
||||||
showSizeChanger={false}
|
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">
|
<div className="flex items-center space-x-2">
|
||||||
<Input
|
<Input
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -117,8 +138,8 @@ export const StaffTable: React.FC<StaffTableProps> = ({
|
||||||
className="text-center"
|
className="text-center"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
|
||||||
size="small"
|
size="small"
|
||||||
|
type="primary"
|
||||||
onClick={handleJumpPage}
|
onClick={handleJumpPage}
|
||||||
>
|
>
|
||||||
跳转
|
跳转
|
||||||
|
|
|
@ -100,7 +100,7 @@ server {
|
||||||
# 仅供内部使用
|
# 仅供内部使用
|
||||||
internal;
|
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;
|
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