Add employee code assignment function

This commit is contained in:
2026-03-02 12:58:05 +08:00
parent d2dac6091e
commit 76ea5a2e06
9 changed files with 1068 additions and 160 deletions

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { Card, Table, Input, Select, Button, Space, message, Modal, Tag, Spin } from 'antd';
import { TeamOutlined, DeleteOutlined, EyeOutlined, StopOutlined } from '@ant-design/icons';
import type { Company } from '@/types';
import { Card, Table, Input, Button, Space, message, Modal, Tag, Spin, Form, Radio, InputNumber, DatePicker, ColorPicker, Pagination } from 'antd';
import { TeamOutlined, DeleteOutlined, EyeOutlined, StopOutlined, PlusOutlined } from '@ant-design/icons';
import QRCode from 'qrcode';
import { useNavigate } from 'react-router-dom';
import type { Color } from 'antd/es/color-picker';
interface CompanyData {
companyName: string;
@@ -25,16 +25,23 @@ function ManagePage() {
const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false);
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('');
const [selectedSerial, setSelectedSerial] = useState<string>('');
const [generateModalVisible, setGenerateModalVisible] = useState(false);
const [generateLoading, setGenerateLoading] = useState(false);
const [generateForm] = Form.useForm();
const [qrColor, setQrColor] = useState<string>('#000000');
const [generatedData, setGeneratedData] = useState<any>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const navigate = useNavigate();
useEffect(() => {
loadCompanies();
}, [searchTerm]);
}, [searchTerm, page, pageSize]);
const loadCompanies = async () => {
setLoading(true);
try {
// 直接使用 apiClient 来调用后端接口
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
@@ -43,9 +50,9 @@ function ManagePage() {
headers.Authorization = `Bearer ${token}`;
}
let url = '/api/companies';
let url = `/api/companies?page=${page}&limit=${pageSize}`;
if (searchTerm) {
url += `?search=${encodeURIComponent(searchTerm)}`;
url += `&search=${encodeURIComponent(searchTerm)}`;
}
const response = await fetch(url, { headers });
@@ -53,8 +60,10 @@ function ManagePage() {
if (data.data) {
setCompanies(data.data);
setTotal(data.pagination?.total || data.data.length);
} else if (data.message) {
setCompanies([]);
setTotal(0);
} else {
throw new Error(data.error || '获取企业列表失败');
}
@@ -62,6 +71,7 @@ function ManagePage() {
console.error('Load companies error:', error);
message.error(error.message || '加载企业列表失败');
setCompanies([]);
setTotal(0);
} finally {
setLoading(false);
}
@@ -224,6 +234,71 @@ function ManagePage() {
});
};
const handleGenerate = async (values: any) => {
setGenerateLoading(true);
try {
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const payload = {
companyName: values.companyName,
quantity: values.quantity,
validDays: values.validOption === 'days' ? values.validDays : undefined,
serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined,
};
const response = await fetch('/api/serials/generate', {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
const data = await response.json();
if (data.serials) {
setGeneratedData(data);
if (data.serials && data.serials.length > 0) {
const baseUrl = window.location.origin;
const queryUrl = `${baseUrl}/query?serial=${data.serials[0].serialNumber}`;
const qrCode = await QRCode.toDataURL(queryUrl, {
color: {
dark: qrColor,
light: '#ffffff',
},
});
setQrCodeDataUrl(qrCode);
}
message.success('生成成功!');
loadCompanies();
} else {
throw new Error(data.error || '生成失败');
}
} 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 handlePageChange = (newPage: number, newPageSize: number) => {
setPage(newPage);
setPageSize(newPageSize);
};
const columns = [
{
title: '企业名称',
@@ -300,14 +375,19 @@ function ManagePage() {
</Space>
}
extra={
<Input.Search
placeholder="搜索企业"
allowClear
style={{ width: 200 }}
onSearch={setSearchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
<Space>
<Input.Search
placeholder="搜索企业"
allowClear
style={{ width: 200 }}
onSearch={setSearchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setGenerateModalVisible(true)}>
</Button>
</Space>
}
>
<Table
@@ -315,12 +395,18 @@ function ManagePage() {
dataSource={companies}
rowKey="companyName"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total} 家企业`,
}}
pagination={false}
/>
<div style={{ marginTop: 16 }}>
<Pagination
current={page}
pageSize={pageSize}
total={total}
onChange={handlePageChange}
showSizeChanger={true}
showTotal={() => `共计 ${total} 条记录`}
/>
</div>
</Card>
<Modal
@@ -420,6 +506,124 @@ function ManagePage() {
)}
</div>
</Modal>
<Modal
title="生成企业序列号"
open={generateModalVisible}
onCancel={() => {
setGenerateModalVisible(false);
generateForm.resetFields();
}}
footer={null}
width={600}
>
<Form
form={generateForm}
layout="vertical"
onFinish={handleGenerate}
initialValues={{
serialOption: 'auto',
quantity: 1,
validOption: 'days',
validDays: 365,
}}
>
<Form.Item
label="企业名称"
name="companyName"
rules={[{ required: true, message: '请输入企业名称' }]}
>
<Input placeholder="输入企业名称(如:浙江贝凡)" />
</Form.Item>
<Form.Item label="序列号设置" name="serialOption">
<Radio.Group>
<Radio value="auto"></Radio>
<Radio value="custom"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.serialOption !== currentValues.serialOption}
>
{({ getFieldValue }) =>
getFieldValue('serialOption') === 'custom' ? (
<Form.Item
label="自定义前缀"
name="serialPrefix"
rules={[{ max: 10, message: '前缀不能超过10个字符' }]}
>
<Input placeholder="输入自定义前缀MYCOMPANY" maxLength={10} />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item
label="序列号数量"
name="quantity"
rules={[{ required: true, message: '请输入序列号数量' }]}
>
<InputNumber min={1} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="有效期设置" name="validOption">
<Radio.Group>
<Radio value="days"></Radio>
<Radio value="date"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.validOption !== currentValues.validOption}
>
{({ getFieldValue }) =>
getFieldValue('validOption') === 'days' ? (
<Form.Item
label="有效天数"
name="validDays"
rules={[{ required: true, message: '请输入有效天数' }]}
>
<InputNumber min={1} max={3650} style={{ width: '100%' }} />
</Form.Item>
) : (
<Form.Item
label="有效期至"
name="validUntil"
rules={[{ required: true, message: '请选择有效期' }]}
>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
)
}
</Form.Item>
<Form.Item
label="二维码颜色"
name="qrColor"
initialValue="#000000"
>
<ColorPicker
value={qrColor}
onChange={(color: Color) => {
const hexColor = color.toHexString();
setQrColor(hexColor);
}}
/>
</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>
</div>
);
}