feat: 新增多模块功能与服务实现

新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
This commit is contained in:
2026-03-18 09:38:09 +08:00
parent 72cd7f6f45
commit 037e412aad
158 changed files with 50217 additions and 1313 deletions

View File

@@ -0,0 +1,394 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Tag,
Row,
Col,
Statistic,
Alert,
List,
Badge,
Tooltip,
Switch,
message,
Divider,
Timeline,
} from 'antd';
import {
BellOutlined,
ClockCircleOutlined,
WarningOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SettingOutlined,
MailOutlined,
SyncOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
interface ExpiryReminder {
id: string;
certNo: string;
certName: string;
certType: string;
productName: string;
expiryDate: string;
daysRemaining: number;
status: 'CRITICAL' | 'WARNING' | 'NOTICE' | 'SAFE';
reminderSent: boolean;
lastReminderDate?: string;
nextReminderDate?: string;
actions: string[];
}
const MOCK_REMINDERS: ExpiryReminder[] = [
{
id: '1',
certNo: 'FCC-2025-045',
certName: 'FCC Declaration',
certType: 'FCC',
productName: 'Control Module B',
expiryDate: '2025-06-19',
daysRemaining: 93,
status: 'WARNING',
reminderSent: true,
lastReminderDate: '2025-03-15',
nextReminderDate: '2025-04-15',
actions: ['Initiate renewal process', 'Contact testing lab'],
},
{
id: '2',
certNo: 'RoHS-2024-112',
certName: 'RoHS Compliance Certificate',
certType: 'RoHS',
productName: 'Power Supply Unit C',
expiryDate: '2025-02-28',
daysRemaining: -18,
status: 'CRITICAL',
reminderSent: true,
lastReminderDate: '2025-03-01',
actions: ['Immediate re-certification required', 'Suspend product listing'],
},
{
id: '3',
certNo: 'CE-2026-001',
certName: 'CE Declaration of Conformity',
certType: 'CE',
productName: 'Industrial Sensor A',
expiryDate: '2027-01-14',
daysRemaining: 663,
status: 'SAFE',
reminderSent: false,
actions: [],
},
{
id: '4',
certNo: 'UL-2026-008',
certName: 'UL Safety Certification',
certType: 'UL',
productName: 'Communication Gateway D',
expiryDate: '2027-01-31',
daysRemaining: 680,
status: 'SAFE',
reminderSent: false,
actions: [],
},
{
id: '5',
certNo: 'ISO-2025-022',
certName: 'ISO 9001:2015',
certType: 'ISO',
productName: 'Company-wide',
expiryDate: '2025-09-30',
daysRemaining: 196,
status: 'NOTICE',
reminderSent: true,
lastReminderDate: '2025-03-01',
nextReminderDate: '2025-06-01',
actions: ['Schedule surveillance audit', 'Update quality manual'],
},
];
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode; bgColor: string }> = {
CRITICAL: { color: 'error', text: 'Critical', icon: <CloseCircleOutlined />, bgColor: '#fff2f0' },
WARNING: { color: 'warning', text: 'Warning', icon: <WarningOutlined />, bgColor: '#fffbe6' },
NOTICE: { color: 'processing', text: 'Notice', icon: <ClockCircleOutlined />, bgColor: '#e6f7ff' },
SAFE: { color: 'success', text: 'Safe', icon: <CheckCircleOutlined />, bgColor: '#f6ffed' },
};
export const CertificateExpiryReminder: React.FC = () => {
const [loading, setLoading] = useState(false);
const [reminders, setReminders] = useState<ExpiryReminder[]>(MOCK_REMINDERS);
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const [emailEnabled, setEmailEnabled] = useState(true);
const [autoRenewal, setAutoRenewal] = useState(false);
useEffect(() => {
checkReminders();
}, []);
const checkReminders = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
message.success('Reminders checked');
}, 1000);
};
const handleSendReminder = (reminder: ExpiryReminder) => {
Modal.confirm({
title: 'Send Reminder',
content: `Send expiry reminder for ${reminder.certNo}?`,
onOk: () => {
setReminders(reminders.map(r =>
r.id === reminder.id ? { ...r, reminderSent: true, lastReminderDate: new Date().toISOString().split('T')[0] } : r
));
message.success('Reminder sent successfully');
},
});
};
const handleInitiateRenewal = (reminder: ExpiryReminder) => {
Modal.confirm({
title: 'Initiate Renewal',
content: `Start renewal process for ${reminder.certNo}?`,
onOk: () => {
message.success('Renewal process initiated');
},
});
};
const getStatusTag = (status: string, days: number) => {
const config = STATUS_CONFIG[status];
if (days < 0) {
return <Tag color="error" icon={<CloseCircleOutlined />}>EXPIRED</Tag>;
}
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
};
const criticalCount = reminders.filter(r => r.status === 'CRITICAL' || r.daysRemaining < 0).length;
const warningCount = reminders.filter(r => r.status === 'WARNING').length;
const noticeCount = reminders.filter(r => r.status === 'NOTICE').length;
return (
<div className="certificate-expiry-reminder-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="Total Certificates"
value={reminders.length}
prefix={<FileTextOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card style={{ background: STATUS_CONFIG.CRITICAL.bgColor }}>
<Statistic
title="Critical / Expired"
value={criticalCount}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card style={{ background: STATUS_CONFIG.WARNING.bgColor }}>
<Statistic
title="Warning"
value={warningCount}
valueStyle={{ color: '#faad14' }}
prefix={<WarningOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card style={{ background: STATUS_CONFIG.NOTICE.bgColor }}>
<Statistic
title="Notice"
value={noticeCount}
valueStyle={{ color: '#1890ff' }}
prefix={<ClockCircleOutlined />}
/>
</Card>
</Col>
</Row>
{criticalCount > 0 && (
<Alert
message="Critical Certificates Detected"
description={`${criticalCount} certificate(s) have expired or are in critical status. Immediate action required to maintain compliance.`}
type="error"
showIcon
icon={<CloseCircleOutlined />}
style={{ marginBottom: 24 }}
action={
<Button size="small" danger onClick={() => message.info('Navigating to critical certificates...')}>
View Details
</Button>
}
/>
)}
<Card
title="Certificate Expiry Reminders"
extra={
<Space>
<Button icon={<SyncOutlined />} onClick={checkReminders} loading={loading}>
Refresh
</Button>
<Button icon={<SettingOutlined />} onClick={() => setSettingsModalVisible(true)}>
Settings
</Button>
</Space>
}
>
<List
itemLayout="horizontal"
dataSource={reminders.sort((a, b) => a.daysRemaining - b.daysRemaining)}
renderItem={(item) => (
<List.Item
style={{
background: item.daysRemaining < 0 ? '#fff2f0' : item.daysRemaining <= 90 ? '#fffbe6' : 'transparent',
padding: 16,
marginBottom: 8,
borderRadius: 8,
}}
actions={[
<Tooltip title="Send Reminder">
<Button
type="link"
icon={<MailOutlined />}
onClick={() => handleSendReminder(item)}
disabled={item.reminderSent && item.daysRemaining > 30}
/>
</Tooltip>,
<Tooltip title="Initiate Renewal">
<Button
type="link"
icon={<SyncOutlined />}
onClick={() => handleInitiateRenewal(item)}
disabled={item.status === 'SAFE'}
/>
</Tooltip>,
]}
>
<List.Item.Meta
avatar={
<Badge
count={item.daysRemaining < 0 ? '!' : item.daysRemaining <= 30 ? item.daysRemaining : 0}
offset={[-5, 5]}
>
<FileTextOutlined style={{ fontSize: 32, color: item.daysRemaining < 0 ? '#ff4d4f' : item.daysRemaining <= 90 ? '#faad14' : '#52c41a' }} />
</Badge>
}
title={
<Space>
<span>{item.certNo}</span>
{getStatusTag(item.status, item.daysRemaining)}
{item.reminderSent && <Tag icon={<BellOutlined />} color="blue">Reminder Sent</Tag>}
</Space>
}
description={
<>
<div>{item.certName} - {item.productName}</div>
<Space style={{ marginTop: 8 }}>
<span>Expiry: {item.expiryDate}</span>
<Divider type="vertical" />
<span style={{
color: item.daysRemaining < 0 ? '#ff4d4f' : item.daysRemaining <= 90 ? '#faad14' : '#52c41a',
fontWeight: 'bold'
}}>
{item.daysRemaining < 0
? `Expired ${Math.abs(item.daysRemaining)} days ago`
: `${item.daysRemaining} days remaining`
}
</span>
</Space>
</>
}
/>
{item.actions.length > 0 && (
<div style={{ marginLeft: 48 }}>
<Timeline
items={item.actions.map((action, index) => ({
color: index === 0 ? 'red' : 'gray',
children: action,
}))}
/>
</div>
)}
</List.Item>
)}
/>
</Card>
<Modal
title="Reminder Settings"
open={settingsModalVisible}
onCancel={() => setSettingsModalVisible(false)}
footer={[
<Button key="close" onClick={() => setSettingsModalVisible(false)}>Close</Button>,
<Button key="save" type="primary" onClick={() => { message.success('Settings saved'); setSettingsModalVisible(false); }}>
Save Settings
</Button>,
]}
>
<div style={{ padding: '16px 0' }}>
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>Email Notifications</div>
<Switch
checked={emailEnabled}
onChange={setEmailEnabled}
checkedChildren="Enabled"
unCheckedChildren="Disabled"
/>
<div style={{ color: '#666', marginTop: 8 }}>
Receive email notifications for certificate expiry reminders
</div>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>Auto-initiate Renewal</div>
<Switch
checked={autoRenewal}
onChange={setAutoRenewal}
checkedChildren="Enabled"
unCheckedChildren="Disabled"
/>
<div style={{ color: '#666', marginTop: 8 }}>
Automatically initiate renewal process when certificate enters warning status
</div>
</div>
<Divider>Reminder Schedule</Divider>
<List
size="small"
dataSource={[
{ status: 'Critical', timing: '90 days before expiry', frequency: 'Weekly' },
{ status: 'Warning', timing: '60 days before expiry', frequency: 'Bi-weekly' },
{ status: 'Notice', timing: '30 days before expiry', frequency: 'Weekly' },
{ status: 'Final', timing: '7 days before expiry', frequency: 'Daily' },
]}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
title={item.status}
description={`${item.timing} - ${item.frequency}`}
/>
</List.Item>
)}
/>
</div>
</Modal>
</div>
);
};
export default CertificateExpiryReminder;

View File

@@ -0,0 +1,538 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
DatePicker,
Upload,
Descriptions,
Divider,
message,
Tag,
Tabs,
Row,
Col,
Statistic,
Alert,
Tooltip,
Badge,
} from 'antd';
import {
FileTextOutlined,
PlusOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
WarningOutlined,
BellOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
const { Option } = Select;
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
interface Certificate {
id: string;
certNo: string;
certName: string;
certType: 'CE' | 'FCC' | 'RoHS' | 'UL' | 'ISO' | 'FDA' | 'CCC' | 'OTHER';
productId: string;
productName: string;
issuer: string;
issueDate: string;
expiryDate: string;
status: 'VALID' | 'EXPIRED' | 'PENDING_RENEWAL' | 'REVOKED';
attachments: string[];
scope: string;
remark?: string;
createdAt: string;
updatedAt: string;
}
const CERT_TYPES = [
{ value: 'CE', label: 'CE Certification', color: 'blue' },
{ value: 'FCC', label: 'FCC Certification', color: 'green' },
{ value: 'RoHS', label: 'RoHS Compliance', color: 'purple' },
{ value: 'UL', label: 'UL Certification', color: 'orange' },
{ value: 'ISO', label: 'ISO Certification', color: 'cyan' },
{ value: 'FDA', label: 'FDA Approval', color: 'red' },
{ value: 'CCC', label: 'CCC Certification', color: 'gold' },
{ value: 'OTHER', label: 'Other', color: 'default' },
];
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
VALID: { color: 'success', text: 'Valid', icon: <CheckCircleOutlined /> },
EXPIRED: { color: 'error', text: 'Expired', icon: <CloseCircleOutlined /> },
PENDING_RENEWAL: { color: 'warning', text: 'Pending Renewal', icon: <ClockCircleOutlined /> },
REVOKED: { color: 'red', text: 'Revoked', icon: <CloseCircleOutlined /> },
};
const MOCK_PRODUCTS = [
{ id: 'P001', name: 'Industrial Sensor A' },
{ id: 'P002', name: 'Control Module B' },
{ id: 'P003', name: 'Power Supply Unit C' },
{ id: 'P004', name: 'Communication Gateway D' },
];
export const CertificateManage: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [certificates, setCertificates] = useState<Certificate[]>([]);
const [createModalVisible, setCreateModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedCert, setSelectedCert] = useState<Certificate | null>(null);
const [activeTab, setActiveTab] = useState('all');
useEffect(() => {
fetchCertificates();
}, []);
const fetchCertificates = async () => {
setLoading(true);
try {
const mockCerts: Certificate[] = [
{
id: '1',
certNo: 'CE-2026-001',
certName: 'CE Declaration of Conformity',
certType: 'CE',
productId: 'P001',
productName: 'Industrial Sensor A',
issuer: 'TÜV SÜD',
issueDate: '2025-01-15',
expiryDate: '2027-01-14',
status: 'VALID',
attachments: ['ce_cert_2026_001.pdf', 'test_report.pdf'],
scope: 'Low Voltage Directive (LVD), EMC Directive',
createdAt: '2025-01-15 10:00:00',
updatedAt: '2025-01-15 10:00:00',
},
{
id: '2',
certNo: 'FCC-2025-045',
certName: 'FCC Declaration',
certType: 'FCC',
productId: 'P002',
productName: 'Control Module B',
issuer: 'FCC',
issueDate: '2024-06-20',
expiryDate: '2025-06-19',
status: 'PENDING_RENEWAL',
attachments: ['fcc_cert.pdf'],
scope: 'Part 15B Class B Digital Device',
remark: 'Renewal application submitted',
createdAt: '2024-06-20 14:30:00',
updatedAt: '2025-03-10 09:00:00',
},
{
id: '3',
certNo: 'RoHS-2024-112',
certName: 'RoHS Compliance Certificate',
certType: 'RoHS',
productId: 'P003',
productName: 'Power Supply Unit C',
issuer: 'SGS',
issueDate: '2024-03-01',
expiryDate: '2025-02-28',
status: 'EXPIRED',
attachments: ['rohs_cert.pdf'],
scope: 'RoHS 2.0 (2011/65/EU)',
remark: 'Needs re-certification',
createdAt: '2024-03-01 09:00:00',
updatedAt: '2025-03-01 00:00:00',
},
{
id: '4',
certNo: 'UL-2026-008',
certName: 'UL Safety Certification',
certType: 'UL',
productId: 'P004',
productName: 'Communication Gateway D',
issuer: 'Underwriters Laboratories',
issueDate: '2026-02-01',
expiryDate: '2027-01-31',
status: 'VALID',
attachments: ['ul_cert.pdf', 'safety_test.pdf'],
scope: 'UL 60950-1, Information Technology Equipment',
createdAt: '2026-02-01 11:00:00',
updatedAt: '2026-02-01 11:00:00',
},
];
setCertificates(mockCerts);
} finally {
setLoading(false);
}
};
const handleCreateCert = async (values: any) => {
setLoading(true);
try {
const product = MOCK_PRODUCTS.find(p => p.id === values.productId);
const newCert: Certificate = {
id: `${Date.now()}`,
certNo: `${values.certType}-${new Date().getFullYear()}-${String(certificates.length + 1).padStart(3, '0')}`,
certName: values.certName,
certType: values.certType,
productId: values.productId,
productName: product?.name || '',
issuer: values.issuer,
issueDate: values.issueDate.format('YYYY-MM-DD'),
expiryDate: values.expiryDate.format('YYYY-MM-DD'),
status: 'VALID',
attachments: [],
scope: values.scope,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
setCertificates([newCert, ...certificates]);
message.success('Certificate created successfully');
setCreateModalVisible(false);
form.resetFields();
} finally {
setLoading(false);
}
};
const handleViewDetail = (cert: Certificate) => {
setSelectedCert(cert);
setDetailModalVisible(true);
};
const handleRenew = (cert: Certificate) => {
Modal.confirm({
title: 'Renew Certificate',
content: `Are you sure you want to initiate renewal for ${cert.certNo}?`,
onOk: () => {
setCertificates(certificates.map(c =>
c.id === cert.id ? { ...c, status: 'PENDING_RENEWAL' as const, updatedAt: new Date().toISOString() } : c
));
message.success('Renewal initiated');
},
});
};
const handleDelete = (certId: string) => {
Modal.confirm({
title: 'Delete Certificate',
content: 'Are you sure you want to delete this certificate?',
okType: 'danger',
onOk: () => {
setCertificates(certificates.filter(c => c.id !== certId));
message.success('Certificate deleted');
},
});
};
const getFilteredCerts = () => {
if (activeTab === 'all') return certificates;
if (activeTab === 'expiring') {
return certificates.filter(c => {
const daysUntilExpiry = dayjs(c.expiryDate).diff(dayjs(), 'day');
return daysUntilExpiry > 0 && daysUntilExpiry <= 90;
});
}
return certificates.filter(c => c.status === activeTab.toUpperCase());
};
const columns: ColumnsType<Certificate> = [
{
title: 'Cert No',
dataIndex: 'certNo',
key: 'certNo',
width: 130,
render: (no: string, record: Certificate) => (
<a onClick={() => handleViewDetail(record)}>{no}</a>
),
},
{
title: 'Certificate Name',
dataIndex: 'certName',
key: 'certName',
width: 200,
ellipsis: true,
},
{
title: 'Type',
dataIndex: 'certType',
key: 'certType',
width: 100,
render: (type: string) => {
const config = CERT_TYPES.find(t => t.value === type);
return <Tag color={config?.color}>{type}</Tag>;
},
},
{
title: 'Product',
dataIndex: 'productName',
key: 'productName',
width: 160,
},
{
title: 'Issuer',
dataIndex: 'issuer',
key: 'issuer',
width: 120,
},
{
title: 'Issue Date',
dataIndex: 'issueDate',
key: 'issueDate',
width: 100,
},
{
title: 'Expiry Date',
dataIndex: 'expiryDate',
key: 'expiryDate',
width: 100,
render: (date: string, record: Certificate) => {
const daysUntilExpiry = dayjs(date).diff(dayjs(), 'day');
const isExpiringSoon = daysUntilExpiry > 0 && daysUntilExpiry <= 90;
return (
<Tooltip title={isExpiringSoon ? `Expires in ${daysUntilExpiry} days` : ''}>
<span style={{ color: isExpiringSoon || daysUntilExpiry <= 0 ? '#ff4d4f' : undefined }}>
{date}
</span>
</Tooltip>
);
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status: string) => {
const config = STATUS_CONFIG[status];
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
},
},
{
title: 'Actions',
key: 'actions',
width: 150,
render: (_, record: Certificate) => (
<Space>
<Tooltip title="View Details">
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
</Tooltip>
{record.status === 'VALID' && (
<Tooltip title="Renew">
<Button type="link" icon={<ClockCircleOutlined />} onClick={() => handleRenew(record)} />
</Tooltip>
)}
<Tooltip title="Delete">
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
</Tooltip>
</Space>
),
},
];
const stats = {
total: certificates.length,
valid: certificates.filter(c => c.status === 'VALID').length,
expiring: certificates.filter(c => {
const days = dayjs(c.expiryDate).diff(dayjs(), 'day');
return days > 0 && days <= 90;
}).length,
expired: certificates.filter(c => c.status === 'EXPIRED').length,
};
return (
<div className="certificate-manage-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}>
<Card>
<Statistic title="Total Certificates" value={stats.total} prefix={<FileTextOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="Valid" value={stats.valid} valueStyle={{ color: '#52c41a' }} prefix={<CheckCircleOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="Expiring Soon" value={stats.expiring} valueStyle={{ color: '#faad14' }} prefix={<WarningOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="Expired" value={stats.expired} valueStyle={{ color: '#ff4d4f' }} prefix={<CloseCircleOutlined />} />
</Card>
</Col>
<Col span={8}>
<Card>
<Alert
message="Certificate Expiry Reminder"
description={`${stats.expiring} certificates are expiring within 90 days. Please initiate renewal process.`}
type="warning"
showIcon
icon={<BellOutlined />}
/>
</Card>
</Col>
</Row>
<Card title="Certificate Management" extra={
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
Add Certificate
</Button>
}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="All" key="all" />
<TabPane tab={<Badge count={stats.expiring} offset={[10, 0]} size="small">Expiring Soon</Badge>} key="expiring" />
<TabPane tab="Valid" key="valid" />
<TabPane tab="Expired" key="expired" />
<TabPane tab="Pending Renewal" key="pending_renewal" />
</Tabs>
<Table
columns={columns}
dataSource={getFilteredCerts()}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
</Card>
<Modal
title="Add New Certificate"
open={createModalVisible}
onCancel={() => setCreateModalVisible(false)}
footer={null}
width={700}
>
<Form form={form} layout="vertical" onFinish={handleCreateCert}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="certType" label="Certificate Type" rules={[{ required: true }]}>
<Select placeholder="Select Type">
{CERT_TYPES.map(t => (
<Option key={t.value} value={t.value}>{t.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="productId" label="Product" rules={[{ required: true }]}>
<Select placeholder="Select Product">
{MOCK_PRODUCTS.map(p => (
<Option key={p.id} value={p.id}>{p.name}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="certName" label="Certificate Name" rules={[{ required: true }]}>
<Input placeholder="Enter certificate name" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="issuer" label="Issuing Authority" rules={[{ required: true }]}>
<Input placeholder="Enter issuer name" />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="issueDate" label="Issue Date" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="expiryDate" label="Expiry Date" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item name="scope" label="Scope">
<Input.TextArea rows={3} placeholder="Enter certification scope" />
</Form.Item>
<Form.Item name="attachments" label="Attachments">
<Upload>
<Button icon={<UploadOutlined />}>Upload Files</Button>
</Upload>
</Form.Item>
<div style={{ textAlign: 'right' }}>
<Button onClick={() => setCreateModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={loading}>Create Certificate</Button>
</div>
</Form>
</Modal>
<Modal
title="Certificate Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
selectedCert?.status === 'VALID' && (
<Button key="renew" type="primary" icon={<ClockCircleOutlined />} onClick={() => { handleRenew(selectedCert); setDetailModalVisible(false); }}>
Initiate Renewal
</Button>
),
]}
width={700}
>
{selectedCert && (
<>
<Descriptions bordered column={2}>
<Descriptions.Item label="Certificate No">{selectedCert.certNo}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={STATUS_CONFIG[selectedCert.status].color}>
{STATUS_CONFIG[selectedCert.status].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Certificate Name" span={2}>{selectedCert.certName}</Descriptions.Item>
<Descriptions.Item label="Type">
<Tag color={CERT_TYPES.find(t => t.value === selectedCert.certType)?.color}>
{CERT_TYPES.find(t => t.value === selectedCert.certType)?.label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Product">{selectedCert.productName}</Descriptions.Item>
<Descriptions.Item label="Issuer">{selectedCert.issuer}</Descriptions.Item>
<Descriptions.Item label="Issue Date">{selectedCert.issueDate}</Descriptions.Item>
<Descriptions.Item label="Expiry Date">{selectedCert.expiryDate}</Descriptions.Item>
<Descriptions.Item label="Scope" span={2}>{selectedCert.scope}</Descriptions.Item>
{selectedCert.remark && (
<Descriptions.Item label="Remark" span={2}>{selectedCert.remark}</Descriptions.Item>
)}
</Descriptions>
{selectedCert.attachments.length > 0 && (
<>
<Divider>Attachments</Divider>
<Space>
{selectedCert.attachments.map((file, index) => (
<Button key={index} icon={<DownloadOutlined />} size="small">
{file}
</Button>
))}
</Space>
</>
)}
</>
)}
</Modal>
</div>
);
};
export default CertificateManage;

View File

@@ -0,0 +1,508 @@
import React, { useState } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
Descriptions,
Divider,
message,
Tag,
Row,
Col,
Statistic,
Progress,
Alert,
Tabs,
Tooltip,
} from 'antd';
import {
SafetyCertificateOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
WarningOutlined,
LoadingOutlined,
SyncOutlined,
FileSearchOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Option } = Select;
const { TabPane } = Tabs;
interface ComplianceResult {
id: string;
productId: string;
productName: string;
platform: string;
checkDate: string;
overallStatus: 'PASS' | 'FAIL' | 'WARNING' | 'PENDING';
checks: {
certCompliance: boolean;
labelCompliance: boolean;
descriptionCompliance: boolean;
imageCompliance: boolean;
priceCompliance: boolean;
policyCompliance: boolean;
};
issues: string[];
score: number;
}
const PLATFORMS = [
{ value: 'Amazon', label: 'Amazon', rules: 15 },
{ value: 'eBay', label: 'eBay', rules: 12 },
{ value: 'Shopify', label: 'Shopify', rules: 10 },
{ value: 'Walmart', label: 'Walmart', rules: 14 },
{ value: 'AliExpress', label: 'AliExpress', rules: 11 },
];
const MOCK_PRODUCTS = [
{ id: 'P001', name: 'Industrial Sensor A', hasCert: true },
{ id: 'P002', name: 'Control Module B', hasCert: true },
{ id: 'P003', name: 'Power Supply Unit C', hasCert: false },
{ id: 'P004', name: 'Communication Gateway D', hasCert: true },
];
const MOCK_RESULTS: ComplianceResult[] = [
{
id: '1',
productId: 'P001',
productName: 'Industrial Sensor A',
platform: 'Amazon',
checkDate: '2026-03-18 10:30:00',
overallStatus: 'PASS',
checks: {
certCompliance: true,
labelCompliance: true,
descriptionCompliance: true,
imageCompliance: true,
priceCompliance: true,
policyCompliance: true,
},
issues: [],
score: 100,
},
{
id: '2',
productId: 'P002',
productName: 'Control Module B',
platform: 'Amazon',
checkDate: '2026-03-18 09:15:00',
overallStatus: 'WARNING',
checks: {
certCompliance: true,
labelCompliance: true,
descriptionCompliance: false,
imageCompliance: true,
priceCompliance: true,
policyCompliance: false,
},
issues: [
'Product description missing warranty information',
'Contains restricted keyword in bullet points',
],
score: 75,
},
{
id: '3',
productId: 'P003',
productName: 'Power Supply Unit C',
platform: 'eBay',
checkDate: '2026-03-17 16:00:00',
overallStatus: 'FAIL',
checks: {
certCompliance: false,
labelCompliance: true,
descriptionCompliance: false,
imageCompliance: false,
priceCompliance: true,
policyCompliance: true,
},
issues: [
'Missing required CE certification',
'Product images missing watermark',
'Description contains promotional content',
],
score: 40,
},
];
export const ComplianceCheck: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<ComplianceResult[]>(MOCK_RESULTS);
const [checkModalVisible, setCheckModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedResult, setSelectedResult] = useState<ComplianceResult | null>(null);
const [activeTab, setActiveTab] = useState('all');
const handleRunCheck = async (values: any) => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 2000));
const product = MOCK_PRODUCTS.find(p => p.id === values.productId);
const hasCert = product?.hasCert || false;
const newResult: ComplianceResult = {
id: `${Date.now()}`,
productId: values.productId,
productName: product?.name || '',
platform: values.platform,
checkDate: new Date().toISOString(),
overallStatus: hasCert ? 'PASS' : 'FAIL',
checks: {
certCompliance: hasCert,
labelCompliance: true,
descriptionCompliance: hasCert,
imageCompliance: true,
priceCompliance: true,
policyCompliance: hasCert,
},
issues: hasCert ? [] : ['Missing required certification'],
score: hasCert ? 100 : 30,
};
setResults([newResult, ...results]);
message.success('Compliance check completed');
setCheckModalVisible(false);
form.resetFields();
} finally {
setLoading(false);
}
};
const handleViewDetail = (result: ComplianceResult) => {
setSelectedResult(result);
setDetailModalVisible(true);
};
const getStatusConfig = (status: string) => {
const configs: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
PASS: { color: 'success', text: 'Pass', icon: <CheckCircleOutlined /> },
FAIL: { color: 'error', text: 'Fail', icon: <CloseCircleOutlined /> },
WARNING: { color: 'warning', text: 'Warning', icon: <WarningOutlined /> },
PENDING: { color: 'processing', text: 'Pending', icon: <LoadingOutlined /> },
};
return configs[status];
};
const getFilteredResults = () => {
if (activeTab === 'all') return results;
return results.filter(r => r.overallStatus.toLowerCase() === activeTab);
};
const columns: ColumnsType<ComplianceResult> = [
{
title: 'Product ID',
dataIndex: 'productId',
key: 'productId',
width: 100,
},
{
title: 'Product Name',
dataIndex: 'productName',
key: 'productName',
width: 180,
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
render: (platform: string) => <Tag>{platform}</Tag>,
},
{
title: 'Score',
dataIndex: 'score',
key: 'score',
width: 150,
render: (score: number, record: ComplianceResult) => (
<Tooltip title={`${record.overallStatus}`}>
<Progress
percent={score}
size="small"
status={record.overallStatus === 'PASS' ? 'success' : record.overallStatus === 'FAIL' ? 'exception' : 'normal'}
strokeColor={record.overallStatus === 'PASS' ? '#52c41a' : record.overallStatus === 'FAIL' ? '#ff4d4f' : '#faad14'}
/>
</Tooltip>
),
},
{
title: 'Status',
dataIndex: 'overallStatus',
key: 'overallStatus',
width: 100,
render: (status: string) => {
const config = getStatusConfig(status);
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
},
},
{
title: 'Check Date',
dataIndex: 'checkDate',
key: 'checkDate',
width: 160,
},
{
title: 'Issues',
dataIndex: 'issues',
key: 'issues',
width: 100,
render: (issues: string[]) => issues.length > 0 ? (
<Tag color="error">{issues.length} issues</Tag>
) : (
<Tag color="success">No issues</Tag>
),
},
{
title: 'Actions',
key: 'actions',
width: 100,
render: (_, record) => (
<Button type="link" icon={<FileSearchOutlined />} onClick={() => handleViewDetail(record)}>
Details
</Button>
),
},
];
const stats = {
total: results.length,
pass: results.filter(r => r.overallStatus === 'PASS').length,
warning: results.filter(r => r.overallStatus === 'WARNING').length,
fail: results.filter(r => r.overallStatus === 'FAIL').length,
};
return (
<div className="compliance-check-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="Total Checks" value={stats.total} prefix={<SafetyCertificateOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Passed"
value={stats.pass}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Warnings"
value={stats.warning}
valueStyle={{ color: '#faad14' }}
prefix={<WarningOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Failed"
value={stats.fail}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Card>
</Col>
</Row>
<Card title="Compliance Check Results" extra={
<Button type="primary" icon={<SyncOutlined />} onClick={() => setCheckModalVisible(true)}>
Run New Check
</Button>
}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="All" key="all" />
<TabPane tab="Pass" key="pass" />
<TabPane tab="Warning" key="warning" />
<TabPane tab="Fail" key="fail" />
</Tabs>
<Table
columns={columns}
dataSource={getFilteredResults()}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
</Card>
<Modal
title="Run Compliance Check"
open={checkModalVisible}
onCancel={() => setCheckModalVisible(false)}
footer={null}
width={500}
>
<Form form={form} layout="vertical" onFinish={handleRunCheck}>
<Form.Item name="productId" label="Product" rules={[{ required: true }]}>
<Select placeholder="Select Product">
{MOCK_PRODUCTS.map(p => (
<Option key={p.id} value={p.id}>
{p.id} - {p.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
<Select placeholder="Select Platform">
{PLATFORMS.map(p => (
<Option key={p.value} value={p.value}>
{p.label} ({p.rules} rules)
</Option>
))}
</Select>
</Form.Item>
<Alert
message="Compliance Check Scope"
description="The check will validate your product against platform-specific rules including certification requirements, labeling standards, description guidelines, image requirements, pricing policies, and platform policies."
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<div style={{ textAlign: 'right' }}>
<Button onClick={() => setCheckModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={loading} icon={<SafetyCertificateOutlined />}>
Start Check
</Button>
</div>
</Form>
</Modal>
<Modal
title="Compliance Check Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
]}
width={700}
>
{selectedResult && (
<>
<Descriptions bordered column={2} style={{ marginBottom: 24 }}>
<Descriptions.Item label="Product">{selectedResult.productName}</Descriptions.Item>
<Descriptions.Item label="Product ID">{selectedResult.productId}</Descriptions.Item>
<Descriptions.Item label="Platform">
<Tag>{selectedResult.platform}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Check Date">{selectedResult.checkDate}</Descriptions.Item>
<Descriptions.Item label="Overall Status">
<Tag color={getStatusConfig(selectedResult.overallStatus).color}>
{getStatusConfig(selectedResult.overallStatus).text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Compliance Score">
<Progress
percent={selectedResult.score}
status={selectedResult.overallStatus === 'PASS' ? 'success' : 'exception'}
style={{ width: 150 }}
/>
</Descriptions.Item>
</Descriptions>
<Divider>Compliance Checks</Divider>
<Row gutter={[16, 16]}>
<Col span={8}>
<Card size="small">
<Statistic
title="Certification Compliance"
value={selectedResult.checks.certCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.certCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.certCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic
title="Label Compliance"
value={selectedResult.checks.labelCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.labelCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.labelCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic
title="Description Compliance"
value={selectedResult.checks.descriptionCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.descriptionCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.descriptionCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic
title="Image Compliance"
value={selectedResult.checks.imageCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.imageCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.imageCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic
title="Price Compliance"
value={selectedResult.checks.priceCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.priceCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.priceCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic
title="Policy Compliance"
value={selectedResult.checks.policyCompliance ? 'PASS' : 'FAIL'}
valueStyle={{ color: selectedResult.checks.policyCompliance ? '#52c41a' : '#ff4d4f' }}
prefix={selectedResult.checks.policyCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
/>
</Card>
</Col>
</Row>
{selectedResult.issues.length > 0 && (
<>
<Divider>Issues Found</Divider>
<Alert
type="error"
message={`${selectedResult.issues.length} Issues Detected`}
description={
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
{selectedResult.issues.map((issue, index) => (
<li key={index}>{issue}</li>
))}
</ul>
}
/>
</>
)}
</>
)}
</Modal>
</div>
);
};
export default ComplianceCheck;

View File

@@ -0,0 +1,3 @@
export { CertificateManage } from './CertificateManage';
export { ComplianceCheck } from './ComplianceCheck';
export { CertificateExpiryReminder } from './CertificateExpiryReminder';