534 lines
16 KiB
TypeScript
534 lines
16 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Card, Table, Input, Button, Space, message, Modal, Tag, Form, Select, InputNumber, Pagination, ColorPicker } from 'antd';
|
|
import { UserOutlined, PlusOutlined, StopOutlined, EditOutlined, QrcodeOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
import { employeeSerialApi } from '@/services/api';
|
|
import QRCode from 'qrcode';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import type { Color } from 'antd/es/color-picker';
|
|
import type { EmployeeSerial } from '@/types';
|
|
|
|
function EmployeeSerialsPage() {
|
|
const [serials, setSerials] = useState<EmployeeSerial[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [limit, setLimit] = useState(10);
|
|
const [total, setTotal] = useState(0);
|
|
const [generateModalVisible, setGenerateModalVisible] = useState(false);
|
|
const [generateLoading, setGenerateLoading] = useState(false);
|
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
const [editLoading, setEditLoading] = useState(false);
|
|
const [selectedSerial, setSelectedSerial] = useState<EmployeeSerial | null>(null);
|
|
const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false);
|
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
|
const [generateForm] = Form.useForm();
|
|
const [editForm] = Form.useForm();
|
|
const [qrColor, setQrColor] = useState('#000000');
|
|
const [generatedData, setGeneratedData] = useState<any>(null);
|
|
const [generateSuccessModalVisible, setGenerateSuccessModalVisible] = useState(false);
|
|
const navigate = useNavigate();
|
|
|
|
const colorPresets = [
|
|
'#000000',
|
|
'#165DFF',
|
|
'#52C41A',
|
|
'#FAAD14',
|
|
'#FF4D4F',
|
|
'#722ED1',
|
|
'#EB2F96',
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadSerials();
|
|
}, [page, limit, searchTerm]);
|
|
|
|
const handlePageChange = (newPage: number, newLimit: number) => {
|
|
setPage(newPage);
|
|
setLimit(newLimit);
|
|
};
|
|
|
|
const loadSerials = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const result = await employeeSerialApi.list({ page, limit, search: searchTerm || undefined });
|
|
setSerials(result.data);
|
|
setTotal(result.pagination.total);
|
|
} catch (error: any) {
|
|
message.error(error.message || '加载员工序列号列表失败');
|
|
setSerials([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGenerate = async (values: { companyName: string; department: string; employeeName: string; quantity: number }) => {
|
|
setGenerateLoading(true);
|
|
try {
|
|
const result = await employeeSerialApi.generate(values);
|
|
|
|
if (result.serials && result.serials.length > 0) {
|
|
const baseUrl = window.location.origin;
|
|
const queryUrl = `${baseUrl}/query?serial=${result.serials[0].serialNumber}`;
|
|
const qrCode = await QRCode.toDataURL(queryUrl, {
|
|
color: {
|
|
dark: qrColor,
|
|
light: '#ffffff',
|
|
},
|
|
});
|
|
setQrCodeDataUrl(qrCode);
|
|
setGeneratedData(result);
|
|
setGenerateSuccessModalVisible(true);
|
|
}
|
|
|
|
message.success(result.message || '生成成功');
|
|
setGenerateModalVisible(false);
|
|
generateForm.resetFields();
|
|
loadSerials();
|
|
} catch (error: any) {
|
|
message.error(error.message || '生成失败');
|
|
} finally {
|
|
setGenerateLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDownloadQR = () => {
|
|
const link = document.createElement('a');
|
|
link.download = `qrcode-${generatedData?.serials?.[0]?.serialNumber}.png`;
|
|
link.href = qrCodeDataUrl;
|
|
link.click();
|
|
};
|
|
|
|
const handleViewQuery = () => {
|
|
if (generatedData?.serials?.[0]?.serialNumber) {
|
|
navigate(`/query?serial=${generatedData.serials[0].serialNumber}`);
|
|
setGenerateSuccessModalVisible(false);
|
|
generateForm.resetFields();
|
|
}
|
|
};
|
|
|
|
const handleEdit = (serial: EmployeeSerial) => {
|
|
setSelectedSerial(serial);
|
|
editForm.setFieldsValue({
|
|
companyName: serial.companyName,
|
|
department: serial.department,
|
|
employeeName: serial.employeeName,
|
|
isActive: serial.isActive,
|
|
});
|
|
setEditModalVisible(true);
|
|
};
|
|
|
|
const handleUpdate = async (values: { companyName?: string; department?: string; employeeName?: string; isActive?: boolean }) => {
|
|
if (!selectedSerial) return;
|
|
setEditLoading(true);
|
|
try {
|
|
await employeeSerialApi.update(selectedSerial.serialNumber, values);
|
|
message.success('更新成功');
|
|
setEditModalVisible(false);
|
|
loadSerials();
|
|
} catch (error: any) {
|
|
message.error(error.message || '更新失败');
|
|
} finally {
|
|
setEditLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRevoke = async (serial: EmployeeSerial) => {
|
|
Modal.confirm({
|
|
title: '确认吊销',
|
|
content: `确定要吊销序列号 "${serial.serialNumber}" 吗?`,
|
|
okText: '确定',
|
|
okType: 'danger',
|
|
cancelText: '取消',
|
|
onOk: async () => {
|
|
try {
|
|
await employeeSerialApi.revoke(serial.serialNumber);
|
|
message.success('吊销成功');
|
|
loadSerials();
|
|
} catch (error: any) {
|
|
message.error(error.message || '吊销失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleDelete = async (serial: EmployeeSerial) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: `确定要删除序列号 "${serial.serialNumber}" 吗?此操作不可恢复!`,
|
|
okText: '确定',
|
|
okType: 'danger',
|
|
cancelText: '取消',
|
|
onOk: async () => {
|
|
try {
|
|
await employeeSerialApi.delete(serial.serialNumber);
|
|
message.success('删除成功');
|
|
loadSerials();
|
|
} catch (error: any) {
|
|
message.error(error.message || '删除失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleViewQrCode = async (serial: EmployeeSerial) => {
|
|
setSelectedSerial(serial);
|
|
try {
|
|
const baseUrl = window.location.origin;
|
|
const result = await employeeSerialApi.generateQrCode(serial.serialNumber, `${baseUrl}/query`);
|
|
if (result.qrCodeData) {
|
|
const qrDataUrl = result.qrCodeData.startsWith('data:')
|
|
? result.qrCodeData
|
|
: `data:image/png;base64,${result.qrCodeData}`;
|
|
setQrCodeDataUrl(qrDataUrl);
|
|
setQrCodeModalVisible(true);
|
|
}
|
|
} catch (error: any) {
|
|
message.error(error.message || '生成二维码失败');
|
|
}
|
|
};
|
|
|
|
const handleSearch = (value: string) => {
|
|
setSearchTerm(value);
|
|
setPage(1);
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '序列号',
|
|
dataIndex: 'serialNumber',
|
|
key: 'serialNumber',
|
|
width: 180,
|
|
},
|
|
{
|
|
title: '企业名称',
|
|
dataIndex: 'companyName',
|
|
key: 'companyName',
|
|
},
|
|
{
|
|
title: '部门',
|
|
dataIndex: 'department',
|
|
key: 'department',
|
|
},
|
|
{
|
|
title: '员工姓名',
|
|
dataIndex: 'employeeName',
|
|
key: 'employeeName',
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'isActive',
|
|
key: 'isActive',
|
|
render: (isActive: boolean) => (
|
|
<Tag color={isActive ? 'green' : 'red'}>
|
|
{isActive ? '有效' : '已吊销'}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'createdAt',
|
|
key: 'createdAt',
|
|
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
render: (_: any, record: EmployeeSerial) => (
|
|
<Space>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<QrcodeOutlined />}
|
|
onClick={() => handleViewQrCode(record)}
|
|
>
|
|
二维码
|
|
</Button>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEdit(record)}
|
|
>
|
|
编辑
|
|
</Button>
|
|
{record.isActive && (
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
danger
|
|
icon={<StopOutlined />}
|
|
onClick={() => handleRevoke(record)}
|
|
>
|
|
吊销
|
|
</Button>
|
|
)}
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
onClick={() => handleDelete(record)}
|
|
>
|
|
删除
|
|
</Button>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Card
|
|
title={
|
|
<Space>
|
|
<UserOutlined />
|
|
<span>员工管理</span>
|
|
</Space>
|
|
}
|
|
extra={
|
|
<Space>
|
|
<Input.Search
|
|
placeholder="搜索序列号/企业/部门/员工"
|
|
allowClear
|
|
style={{ width: 250 }}
|
|
onSearch={handleSearch}
|
|
onChange={(e) => {
|
|
if (!e.target.value) {
|
|
handleSearch('');
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => setGenerateModalVisible(true)}
|
|
>
|
|
生成序列号
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={serials}
|
|
rowKey="serialNumber"
|
|
loading={loading}
|
|
pagination={false}
|
|
/>
|
|
<div style={{ marginTop: 16 }}>
|
|
<Pagination
|
|
current={page}
|
|
pageSize={limit}
|
|
total={total}
|
|
onChange={handlePageChange}
|
|
showSizeChanger={true}
|
|
showTotal={(t) => `共计 ${t} 条记录`}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
<Modal
|
|
title="生成员工序列号"
|
|
open={generateModalVisible}
|
|
onCancel={() => {
|
|
setGenerateModalVisible(false);
|
|
generateForm.resetFields();
|
|
}}
|
|
footer={null}
|
|
width={500}
|
|
>
|
|
<Form
|
|
form={generateForm}
|
|
layout="vertical"
|
|
onFinish={handleGenerate}
|
|
>
|
|
<Form.Item
|
|
name="companyName"
|
|
label="企业名称"
|
|
rules={[{ required: true, message: '请输入企业名称' }]}
|
|
>
|
|
<Input placeholder="请输入企业名称" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="department"
|
|
label="部门"
|
|
rules={[{ required: true, message: '请输入部门' }]}
|
|
>
|
|
<Input placeholder="请输入部门" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="employeeName"
|
|
label="员工姓名"
|
|
rules={[{ required: true, message: '请输入员工姓名' }]}
|
|
>
|
|
<Input placeholder="请输入员工姓名" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="quantity"
|
|
label="生成数量"
|
|
rules={[{ required: true, message: '请输入生成数量' }]}
|
|
initialValue={1}
|
|
>
|
|
<InputNumber min={1} max={1000} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="二维码颜色"
|
|
name="qrColor"
|
|
initialValue="#000000"
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
|
{colorPresets.map((color) => (
|
|
<div
|
|
key={color}
|
|
onClick={() => setQrColor(color)}
|
|
style={{
|
|
width: '28px',
|
|
height: '28px',
|
|
backgroundColor: color,
|
|
border: qrColor === color ? '2px solid #165DFF' : '2px solid #d9d9d9',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<ColorPicker
|
|
value={qrColor}
|
|
onChange={(color: Color) => {
|
|
const hexColor = color.toHexString();
|
|
setQrColor(hexColor);
|
|
}}
|
|
/>
|
|
</div>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
<Button onClick={() => setGenerateModalVisible(false)}>取消</Button>
|
|
<Button type="primary" htmlType="submit" loading={generateLoading}>
|
|
生成
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="编辑员工序列号"
|
|
open={editModalVisible}
|
|
onCancel={() => {
|
|
setEditModalVisible(false);
|
|
editForm.resetFields();
|
|
}}
|
|
footer={null}
|
|
width={500}
|
|
>
|
|
<Form
|
|
form={editForm}
|
|
layout="vertical"
|
|
onFinish={handleUpdate}
|
|
>
|
|
<Form.Item
|
|
name="companyName"
|
|
label="企业名称"
|
|
>
|
|
<Input placeholder="请输入企业名称" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="department"
|
|
label="部门"
|
|
>
|
|
<Input placeholder="请输入部门" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="employeeName"
|
|
label="员工姓名"
|
|
>
|
|
<Input placeholder="请输入员工姓名" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="isActive"
|
|
label="状态"
|
|
>
|
|
<Select placeholder="请选择状态">
|
|
<Select.Option value={true}>有效</Select.Option>
|
|
<Select.Option value={false}>吊销</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
<Button onClick={() => setEditModalVisible(false)}>取消</Button>
|
|
<Button type="primary" htmlType="submit" loading={editLoading}>
|
|
保存
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="生成成功"
|
|
open={generateSuccessModalVisible}
|
|
onCancel={() => setGenerateSuccessModalVisible(false)}
|
|
footer={null}
|
|
width={600}
|
|
>
|
|
{generatedData && (
|
|
<div>
|
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
<div>
|
|
<p><strong>企业名称:</strong> {generatedData.serials?.[0]?.companyName}</p>
|
|
<p><strong>部门:</strong> {generatedData.serials?.[0]?.department}</p>
|
|
<p><strong>员工姓名:</strong> {generatedData.serials?.[0]?.employeeName}</p>
|
|
<p><strong>生成数量:</strong> {generatedData.serials?.length || 0}</p>
|
|
</div>
|
|
|
|
{qrCodeDataUrl && (
|
|
<div style={{ textAlign: 'center' }}>
|
|
<img src={qrCodeDataUrl} alt="QR Code" style={{ width: '200px', height: '200px' }} />
|
|
{generatedData.serials && generatedData.serials.length > 0 && (
|
|
<p style={{ marginTop: '12px', fontFamily: 'monospace', fontSize: '16px', fontWeight: 'bold', color: '#165DFF' }}>{generatedData.serials[0].serialNumber}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Space>
|
|
<Button type="primary" onClick={handleViewQuery}>查询序列号</Button>
|
|
<Button onClick={handleDownloadQR}>下载二维码</Button>
|
|
<Button onClick={() => {
|
|
setGenerateSuccessModalVisible(false);
|
|
generateForm.resetFields();
|
|
}}>生成新的序列号</Button>
|
|
</Space>
|
|
</Space>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="员工二维码"
|
|
open={qrCodeModalVisible}
|
|
onCancel={() => setQrCodeModalVisible(false)}
|
|
footer={null}
|
|
width={400}
|
|
>
|
|
<div style={{ textAlign: 'center' }}>
|
|
{qrCodeDataUrl && (
|
|
<>
|
|
<img src={qrCodeDataUrl} alt="QR Code" style={{ width: '200px', height: '200px' }} />
|
|
<p style={{ marginTop: '12px', fontFamily: 'monospace', fontSize: '16px', fontWeight: 'bold', color: '#165DFF' }}>
|
|
{selectedSerial?.serialNumber}
|
|
</p>
|
|
<p style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>
|
|
{selectedSerial?.companyName} - {selectedSerial?.department} - {selectedSerial?.employeeName}
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default EmployeeSerialsPage; |