diff --git a/src/App.tsx b/src/App.tsx
index 2aee437..92f0bb9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import DashboardPage from './pages/Dashboard';
import GeneratePage from './pages/Generate';
import ManagePage from './pages/Manage';
import ProfilePage from './pages/Profile';
+import EmployeeSerialsPage from './pages/EmployeeSerials';
const PrivateRoute = () => {
const user = authApi.getCurrentUser();
@@ -50,6 +51,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx
index 1378d5b..df34039 100644
--- a/src/components/AdminLayout.tsx
+++ b/src/components/AdminLayout.tsx
@@ -8,6 +8,7 @@ import {
LogoutOutlined,
LockOutlined,
ExclamationCircleOutlined,
+ IdcardOutlined,
} from '@ant-design/icons';
import { authApi } from '@/services/api';
import './styles/AdminLayout.css';
@@ -39,6 +40,12 @@ function AdminLayout() {
label: '企业管理',
onClick: () => navigate('/admin/manage'),
},
+ {
+ key: 'employee-serials',
+ icon: ,
+ label: '员工管理',
+ onClick: () => navigate('/admin/employee-serials'),
+ },
];
const handleLogout = () => {
@@ -84,6 +91,7 @@ function AdminLayout() {
if (path.includes('/dashboard')) return 'dashboard';
if (path.includes('/generate')) return 'generate';
if (path.includes('/manage')) return 'manage';
+ if (path.includes('/employee-serials')) return 'employee-serials';
if (path.includes('/profile')) return 'profile';
return 'dashboard';
};
@@ -93,6 +101,7 @@ function AdminLayout() {
if (path.includes('/dashboard')) return '控制台';
if (path.includes('/generate')) return '生成二维码';
if (path.includes('/manage')) return '企业管理';
+ if (path.includes('/employee-serials')) return '员工管理';
if (path.includes('/profile')) return '用户资料';
return '控制台';
};
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 3b6ed0c..55e213b 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
-import { Card, Row, Col, Statistic, Table, Spin, message } from 'antd';
-import { TeamOutlined, KeyOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
+import { Card, Row, Col, Statistic, Table, Spin, message, Tag } from 'antd';
+import { TeamOutlined, KeyOutlined, CheckCircleOutlined, UserOutlined } from '@ant-design/icons';
import { dashboardApi } from '@/services/api';
import type { DashboardStats } from '@/types';
@@ -45,6 +45,16 @@ function DashboardPage() {
/>
+
+
+ }
+ valueStyle={{ color: '#722ed1' }}
+ />
+
+
-
-
- }
- valueStyle={{ color: '#ff4d4f' }}
- />
-
-
@@ -82,6 +82,16 @@ function DashboardPage() {
columns={[
{ title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber' },
{ title: '企业名称', dataIndex: 'companyName', key: 'companyName' },
+ {
+ title: '类型',
+ dataIndex: 'type',
+ key: 'type',
+ render: (type: string) => (
+
+ {type === 'employee' ? '员工' : '企业'}
+
+ ),
+ },
{
title: '状态',
dataIndex: 'status',
diff --git a/src/pages/EmployeeSerials.tsx b/src/pages/EmployeeSerials.tsx
new file mode 100644
index 0000000..9d0e015
--- /dev/null
+++ b/src/pages/EmployeeSerials.tsx
@@ -0,0 +1,416 @@
+import { useEffect, useState } from 'react';
+import { Card, Table, Input, Button, Space, message, Modal, Tag, Form, Select, InputNumber, Pagination } from 'antd';
+import { UserOutlined, PlusOutlined, StopOutlined, EditOutlined, QrcodeOutlined, DeleteOutlined } from '@ant-design/icons';
+import { employeeSerialApi } from '@/services/api';
+import type { EmployeeSerial } from '@/types';
+
+function EmployeeSerialsPage() {
+ const [serials, setSerials] = useState([]);
+ 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(null);
+ const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false);
+ const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
+ const [generateForm] = Form.useForm();
+ const [editForm] = Form.useForm();
+
+ 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);
+ message.success(result.message || '生成成功');
+ setGenerateModalVisible(false);
+ generateForm.resetFields();
+ loadSerials();
+ } catch (error: any) {
+ message.error(error.message || '生成失败');
+ } finally {
+ setGenerateLoading(false);
+ }
+ };
+
+ 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) => (
+
+ {isActive ? '有效' : '已吊销'}
+
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createdAt',
+ key: 'createdAt',
+ render: (date: string) => new Date(date).toLocaleString('zh-CN'),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ render: (_: any, record: EmployeeSerial) => (
+
+ }
+ onClick={() => handleViewQrCode(record)}
+ >
+ 二维码
+
+ }
+ onClick={() => handleEdit(record)}
+ >
+ 编辑
+
+ {record.isActive && (
+ }
+ onClick={() => handleRevoke(record)}
+ >
+ 吊销
+
+ )}
+ }
+ onClick={() => handleDelete(record)}
+ >
+ 删除
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
+
+export default EmployeeSerialsPage;
\ No newline at end of file
diff --git a/src/pages/Generate.tsx b/src/pages/Generate.tsx
index 9476d00..2bdab2d 100644
--- a/src/pages/Generate.tsx
+++ b/src/pages/Generate.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
-import { Form, Input, Button, Card, Radio, InputNumber, DatePicker, message, Modal, Space, ColorPicker, Divider, Row, Col } from 'antd';
-import { QrcodeOutlined } from '@ant-design/icons';
+import { Form, Input, Button, Card, Radio, InputNumber, DatePicker, message, Modal, Space, ColorPicker } from 'antd';
+import { QrcodeOutlined, UserOutlined } from '@ant-design/icons';
import QRCode from 'qrcode';
import type { Color } from 'antd/es/color-picker';
import { useNavigate } from 'react-router-dom';
@@ -13,6 +13,7 @@ function GeneratePage() {
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
const [modalVisible, setModalVisible] = useState(false);
const [qrColor, setQrColor] = useState('#000000');
+ const [generateType, setGenerateType] = useState<'company' | 'employee'>('company');
const navigate = useNavigate();
const colorPresets = [
@@ -36,40 +37,81 @@ function GeneratePage() {
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,
- };
+ let data: any;
- const response = await fetch('/api/serials/generate', {
- method: 'POST',
- headers,
- body: JSON.stringify(payload),
- });
+ if (generateType === 'company') {
+ const payload = {
+ companyName: values.companyName,
+ quantity: values.quantity,
+ validDays: values.validOption === 'days' ? values.validDays : undefined,
+ serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined,
+ };
- const data = await response.json();
+ const response = await fetch('/api/serials/generate', {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(payload),
+ });
- if (data.serials) {
- setGeneratedData(data);
+ data = await response.json();
- 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);
+ 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);
+ }
+
+ setModalVisible(true);
+ message.success('生成成功!');
+ } else {
+ throw new Error(data.error || '生成失败');
}
-
- setModalVisible(true);
- message.success('生成成功!');
} else {
- throw new Error(data.error || '生成失败');
+ const payload = {
+ companyName: values.companyName,
+ department: values.department,
+ employeeName: values.employeeName,
+ quantity: values.quantity,
+ serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined,
+ };
+
+ const response = await fetch('/api/employee-serials/generate', {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(payload),
+ });
+
+ 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);
+ }
+
+ setModalVisible(true);
+ message.success('生成成功!');
+ } else {
+ throw new Error(data.error || '生成失败');
+ }
}
} catch (error: any) {
message.error(error.message || '生成失败');
@@ -104,7 +146,25 @@ function GeneratePage() {
}
bordered={false}
>
-