Initial commit
This commit is contained in:
427
src/pages/Manage.tsx
Normal file
427
src/pages/Manage.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user