This commit is contained in:
Your Name 2025-05-29 21:28:09 +08:00
parent fbbc919ca4
commit 45d2f36b37
6 changed files with 9836 additions and 8128 deletions

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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,6 +24,8 @@ 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">
@ -27,9 +39,15 @@ export const SearchBar: React.FC<SearchBarProps> = ({
/> />
<Button type="primary" onClick={onSearch}></Button> <Button type="primary" onClick={onSearch}></Button>
</Space> </Space>
<Space>
<ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}> <Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button> </Button>
</Space>
</div> </div>
); );
}; };

View File

@ -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}>
<div style={tableContainerStyle}>
<Table <Table
dataSource={data} dataSource={data}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
loading={isLoading} 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}
> >

View File

@ -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;

File diff suppressed because it is too large Load Diff