Initial commit

This commit is contained in:
2026-02-06 14:06:49 +08:00
commit 5fc7b33b3b
28 changed files with 5004 additions and 0 deletions

427
src/pages/Manage.tsx Normal file
View File

@@ -0,0 +1,427 @@
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 QRCode from 'qrcode';
import { useNavigate } from 'react-router-dom';
interface CompanyData {
companyName: string;
serialCount: number;
firstCreated: string;
lastCreated: string;
activeCount: number;
status: 'active' | 'disabled';
}
function ManagePage() {
const [companies, setCompanies] = useState<CompanyData[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCompany, setSelectedCompany] = useState<any>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
const [companyDetail, setCompanyDetail] = useState<any>(null);
const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false);
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('');
const [selectedSerial, setSelectedSerial] = useState<string>('');
const navigate = useNavigate();
useEffect(() => {
loadCompanies();
}, [searchTerm]);
const loadCompanies = async () => {
setLoading(true);
try {
// 直接使用 apiClient 来调用后端接口
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
let url = '/api/companies';
if (searchTerm) {
url += `?search=${encodeURIComponent(searchTerm)}`;
}
const response = await fetch(url, { headers });
const data = await response.json();
if (data.data) {
setCompanies(data.data);
} else if (data.message) {
setCompanies([]);
} else {
throw new Error(data.error || '获取企业列表失败');
}
} catch (error: any) {
console.error('Load companies error:', error);
message.error(error.message || '加载企业列表失败');
setCompanies([]);
} finally {
setLoading(false);
}
};
const handleViewDetail = async (company: CompanyData) => {
setSelectedCompany(company);
setDetailModalVisible(true);
setDetailLoading(true);
try {
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}`, { headers });
const data = await response.json();
if (data.data) {
setCompanyDetail(data.data);
} else {
throw new Error(data.error || '获取企业详情失败');
}
} catch (error: any) {
message.error(error.message || '获取企业详情失败');
} finally {
setDetailLoading(false);
}
};
const handleDelete = async (company: CompanyData) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除企业 "${company.companyName}" 吗?这将删除该企业的所有序列号!`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}`, {
method: 'DELETE',
headers,
});
const data = await response.json();
if (data.message) {
message.success('删除成功');
loadCompanies();
} else {
throw new Error(data.error || '删除失败');
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
},
});
};
const handleRevoke = async (company: CompanyData) => {
Modal.confirm({
title: '确认吊销',
content: `确定要吊销企业 "${company.companyName}" 吗?这将使该企业的所有序列号失效!`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}/revoke`, {
method: 'POST',
headers,
});
const data = await response.json();
if (data.message) {
message.success('吊销成功');
loadCompanies();
} else {
throw new Error(data.error || '吊销失败');
}
} catch (error: any) {
message.error(error.message || '吊销失败');
}
},
});
};
const handleViewQRCode = async (serialNumber: string) => {
try {
const baseUrl = window.location.origin;
const queryUrl = `${baseUrl}/query?serial=${serialNumber}`;
const qrCode = await QRCode.toDataURL(queryUrl);
setQrCodeDataUrl(qrCode);
setSelectedSerial(serialNumber);
setQrCodeModalVisible(true);
} catch (error) {
message.error('生成二维码失败');
}
};
const handleQuerySerial = (serialNumber: string) => {
setQrCodeModalVisible(false);
navigate(`/query?serial=${serialNumber}`);
};
const handleRevokeSerial = async (serialNumber: string) => {
Modal.confirm({
title: '确认吊销',
content: `确定要吊销序列号 "${serialNumber}" 吗?`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const token = localStorage.getItem('authToken');
const headers: any = {
'Content-Type': 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`/api/serials/${encodeURIComponent(serialNumber)}/revoke`, {
method: 'POST',
headers,
});
const data = await response.json();
if (data.message) {
message.success('吊销成功');
if (selectedCompany) {
handleViewDetail(selectedCompany);
}
} else {
throw new Error(data.error || '吊销失败');
}
} catch (error: any) {
message.error(error.message || '吊销失败');
}
},
});
};
const columns = [
{
title: '企业名称',
dataIndex: 'companyName',
key: 'companyName',
},
{
title: '序列号数量',
dataIndex: 'serialCount',
key: 'serialCount',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : 'red'}>
{status === 'active' ? '正常' : '已吊销'}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'firstCreated',
key: 'firstCreated',
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '最后更新',
dataIndex: 'lastCreated',
key: 'lastCreated',
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'actions',
render: (_: any, record: CompanyData) => (
<Space>
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
>
</Button>
<Button
type="link"
danger
icon={<StopOutlined />}
onClick={() => handleRevoke(record)}
>
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
</Button>
</Space>
),
},
];
return (
<div>
<Card
title={
<Space>
<TeamOutlined />
<span></span>
</Space>
}
extra={
<Input.Search
placeholder="搜索企业"
allowClear
style={{ width: 200 }}
onSearch={setSearchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
}
>
<Table
columns={columns}
dataSource={companies}
rowKey="companyName"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total} 家企业`,
}}
/>
</Card>
<Modal
title="企业详情"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={null}
width={800}
>
{detailLoading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Spin size="large" />
</div>
) : companyDetail ? (
<div>
<p><strong>:</strong> {companyDetail.companyName}</p>
<p><strong>:</strong> {companyDetail.serialCount}</p>
<p><strong>:</strong> {companyDetail.activeCount}</p>
<p><strong>:</strong> {companyDetail.disabledCount || 0}</p>
<p><strong>:</strong> {companyDetail.expiredCount || 0}</p>
{companyDetail.serials && companyDetail.serials.length > 0 && (
<div style={{ marginTop: '16px' }}>
<h4></h4>
<Table
columns={[
{ title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber' },
{
title: '状态',
dataIndex: 'isActive',
key: 'isActive',
render: (isActive: boolean) => (
<Tag color={isActive ? 'green' : 'red'}>
{isActive ? '有效' : '已吊销'}
</Tag>
),
},
{
title: '有效期至',
dataIndex: 'validUntil',
key: 'validUntil',
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'actions',
render: (_: any, record: any) => (
<Space>
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewQRCode(record.serialNumber)}
>
</Button>
{record.isActive && (
<Button
type="link"
size="small"
danger
icon={<StopOutlined />}
onClick={() => handleRevokeSerial(record.serialNumber)}
>
</Button>
)}
</Space>
),
},
]}
dataSource={companyDetail.serials}
rowKey="serialNumber"
pagination={false}
size="small"
/>
</div>
)}
</div>
) : null}
</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', cursor: 'pointer' }} onClick={() => handleQuerySerial(selectedSerial)} />
<p style={{ marginTop: '12px', fontFamily: 'monospace', fontSize: '16px', fontWeight: 'bold', color: '#165DFF' }}>{selectedSerial}</p>
<p style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}></p>
</>
)}
</div>
</Modal>
</div>
);
}
export default ManagePage;