feat: 添加MSW模拟服务和数据源集成

refactor: 重构页面组件移除冗余Layout组件

feat: 实现WebSocket和事件总线系统

feat: 添加队列和调度系统

docs: 更新架构文档和服务映射

style: 清理重复接口定义使用数据源

chore: 更新依赖项配置

feat: 添加运行时系统和领域引导

ci: 配置ESLint边界检查规则

build: 添加Redis和WebSocket依赖

test: 添加MSW浏览器环境入口

perf: 优化数据获取逻辑使用统一数据源

fix: 修复类型定义和状态管理问题
This commit is contained in:
2026-03-19 01:39:34 +08:00
parent cd55097dbf
commit 0dac26d781
176 changed files with 47075 additions and 8404 deletions

View File

@@ -0,0 +1,486 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Select,
Switch,
Tag,
Badge,
message,
Popconfirm,
Tooltip,
Row,
Col,
Statistic,
Divider,
Collapse,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined,
CalculatorOutlined,
PercentageOutlined,
DollarOutlined,
SettingOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { settingsDataSource, CostTemplate, CostItem } from '@/services/settingsDataSource';
const { Option } = Select;
const { Panel } = Collapse;
const { TextArea } = Input;
const PLATFORM_LIST = ['AMAZON', 'EBAY', 'SHOPIFY', 'SHOPEE', 'LAZADA', 'ALIBABA', 'ALL'];
const CATEGORY_LIST = ['Electronics', 'Clothing', 'Home', 'Industrial', 'General'];
const COST_TYPE_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
FIXED: { color: 'blue', text: 'Fixed', icon: <DollarOutlined /> },
PERCENTAGE: { color: 'green', text: 'Percentage', icon: <PercentageOutlined /> },
PER_UNIT: { color: 'orange', text: 'Per Unit', icon: <CalculatorOutlined /> },
};
const CostTemplateConfig: React.FC = () => {
const [loading, setLoading] = useState(false);
const [templates, setTemplates] = useState<CostTemplate[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<CostTemplate | null>(null);
const [viewingTemplate, setViewingTemplate] = useState<CostTemplate | null>(null);
const [costItems, setCostItems] = useState<CostItem[]>([]);
const [form] = Form.useForm();
useEffect(() => {
fetchTemplates();
}, []);
const fetchTemplates = async () => {
setLoading(true);
try {
const mockTemplates: CostTemplate[] = [
{
id: '1',
name: 'Amazon US Standard',
platform: 'AMAZON',
category: 'Electronics',
description: 'Standard cost template for Amazon US electronics',
isDefault: true,
isActive: true,
createdAt: '2026-01-15',
updatedAt: '2026-03-10',
costs: [
{ id: 'c1', name: 'Platform Fee', type: 'PERCENTAGE', value: 15, currency: 'USD', applyTo: 'PLATFORM_FEE', description: 'Amazon referral fee' },
{ id: 'c2', name: 'FBA Fee', type: 'PER_UNIT', value: 3.5, currency: 'USD', applyTo: 'SHIPPING', description: 'Fulfillment fee' },
{ id: 'c3', name: 'Storage Fee', type: 'PER_UNIT', value: 0.5, currency: 'USD', applyTo: 'PRODUCT', description: 'Monthly storage' },
],
},
{
id: '2',
name: 'eBay Standard',
platform: 'EBAY',
category: 'General',
description: 'Standard cost template for eBay',
isDefault: true,
isActive: true,
createdAt: '2026-01-20',
updatedAt: '2026-03-05',
costs: [
{ id: 'c4', name: 'Final Value Fee', type: 'PERCENTAGE', value: 12.5, currency: 'USD', applyTo: 'PLATFORM_FEE', description: 'eBay final value fee' },
{ id: 'c5', name: 'PayPal Fee', type: 'PERCENTAGE', value: 2.9, currency: 'USD', applyTo: 'ORDER', description: 'Payment processing fee' },
],
},
{
id: '3',
name: 'Shopify Standard',
platform: 'SHOPIFY',
category: 'General',
description: 'Standard cost template for Shopify stores',
isDefault: true,
isActive: true,
createdAt: '2026-02-01',
updatedAt: '2026-03-15',
costs: [
{ id: 'c6', name: 'Transaction Fee', type: 'PERCENTAGE', value: 2.0, currency: 'USD', applyTo: 'ORDER', description: 'Shopify transaction fee' },
{ id: 'c7', name: 'Payment Gateway', type: 'PERCENTAGE', value: 2.9, currency: 'USD', applyTo: 'ORDER', description: 'Credit card processing' },
],
},
{
id: '4',
name: 'Industrial Equipment',
platform: 'ALL',
category: 'Industrial',
description: 'Cost template for industrial equipment across platforms',
isDefault: false,
isActive: true,
createdAt: '2026-02-15',
updatedAt: '2026-03-18',
costs: [
{ id: 'c8', name: 'Shipping Surcharge', type: 'PERCENTAGE', value: 5, currency: 'USD', applyTo: 'SHIPPING', description: 'Heavy item shipping' },
{ id: 'c9', name: 'Insurance', type: 'PERCENTAGE', value: 1.5, currency: 'USD', applyTo: 'SHIPPING', description: 'Shipping insurance' },
],
},
];
setTemplates(mockTemplates);
} catch (error) {
message.error('Failed to load cost templates');
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingTemplate(null);
setCostItems([]);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (record: CostTemplate) => {
setEditingTemplate(record);
setCostItems(record.costs);
form.setFieldsValue(record);
setModalVisible(true);
};
const handleView = (record: CostTemplate) => {
setViewingTemplate(record);
setDetailModalVisible(true);
};
const handleDelete = (id: string) => {
setTemplates(templates.filter(t => t.id !== id));
message.success('Template deleted successfully');
};
const handleDuplicate = (record: CostTemplate) => {
const newTemplate: CostTemplate = {
...record,
id: `${Date.now()}`,
name: `${record.name} (Copy)`,
isDefault: false,
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
};
setTemplates([...templates, newTemplate]);
message.success('Template duplicated successfully');
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const templateData: CostTemplate = {
...values,
id: editingTemplate?.id || `${Date.now()}`,
costs: costItems,
createdAt: editingTemplate?.createdAt || new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
isActive: true,
};
if (editingTemplate) {
setTemplates(templates.map(t => t.id === editingTemplate.id ? templateData : t));
message.success('Template updated successfully');
} else {
setTemplates([...templates, templateData]);
message.success('Template created successfully');
}
setModalVisible(false);
} catch (error) {
console.error('Form validation failed:', error);
}
};
const handleAddCostItem = () => {
const newItem: CostItem = {
id: `temp-${Date.now()}`,
name: '',
type: 'FIXED',
value: 0,
currency: 'USD',
applyTo: 'PRODUCT',
description: '',
};
setCostItems([...costItems, newItem]);
};
const handleUpdateCostItem = (id: string, field: keyof CostItem, value: any) => {
setCostItems(costItems.map(item =>
item.id === id ? { ...item, [field]: value } : item
));
};
const handleRemoveCostItem = (id: string) => {
setCostItems(costItems.filter(item => item.id !== id));
};
const handleSetDefault = (id: string) => {
setTemplates(templates.map(t => ({
...t,
isDefault: t.id === id,
})));
message.success('Default template updated');
};
const stats = {
total: templates.length,
active: templates.filter(t => t.isActive).length,
defaults: templates.filter(t => t.isDefault).length,
platforms: new Set(templates.map(t => t.platform)).size,
};
const columns: ColumnsType<CostTemplate> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
render: (text, record) => (
<Space>
<a onClick={() => handleView(record)}>{text}</a>
{record.isDefault && <Tag color="gold">Default</Tag>}
</Space>
),
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
render: (platform) => <Tag color="blue">{platform}</Tag>,
},
{
title: 'Category',
dataIndex: 'category',
key: 'category',
width: 100,
},
{
title: 'Cost Items',
key: 'costCount',
width: 100,
render: (_, record) => <Badge count={record.costs.length} style={{ backgroundColor: '#1890ff' }} />,
},
{
title: 'Status',
dataIndex: 'isActive',
key: 'isActive',
width: 80,
render: (active) => <Tag color={active ? 'success' : 'default'}>{active ? 'Active' : 'Inactive'}</Tag>,
},
{
title: 'Updated',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 100,
},
{
title: 'Actions',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Tooltip title="Edit">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</Tooltip>
<Tooltip title="Duplicate">
<Button type="link" size="small" icon={<CopyOutlined />} onClick={() => handleDuplicate(record)} />
</Tooltip>
{!record.isDefault && (
<Tooltip title="Set as Default">
<Button type="link" size="small" onClick={() => handleSetDefault(record.id)}>
Default
</Button>
</Tooltip>
)}
<Popconfirm title="Delete this template?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div className="cost-template-config">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="Total Templates" value={stats.total} prefix={<SettingOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Active" value={stats.active} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Defaults" value={stats.defaults} valueStyle={{ color: '#faad14' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Platforms" value={stats.platforms} valueStyle={{ color: '#1890ff' }} />
</Card>
</Col>
</Row>
<Card
title="Cost Template Configuration"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
Create Template
</Button>
}
>
<Table
columns={columns}
dataSource={templates}
rowKey="id"
loading={loading}
scroll={{ x: 900 }}
pagination={{ showSizeChanger: true, showTotal: (total) => `Total ${total} templates` }}
/>
</Card>
<Modal
title={editingTemplate ? 'Edit Cost Template' : 'Create Cost Template'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={800}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="Template Name" rules={[{ required: true }]}>
<Input placeholder="Enter template name" />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
<Select placeholder="Select platform">
{PLATFORM_LIST.map(p => (
<Option key={p} value={p}>{p}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="category" label="Category" rules={[{ required: true }]}>
<Select placeholder="Select category">
{CATEGORY_LIST.map(c => (
<Option key={c} value={c}>{c}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="description" label="Description">
<TextArea rows={2} placeholder="Enter description" />
</Form.Item>
<Form.Item name="isDefault" label="Set as Default" valuePropName="checked">
<Switch />
</Form.Item>
<Divider>Cost Items</Divider>
<div style={{ marginBottom: 16 }}>
<Button icon={<PlusOutlined />} onClick={handleAddCostItem}>
Add Cost Item
</Button>
</div>
{costItems.map((item, index) => (
<Card key={item.id} size="small" style={{ marginBottom: 8 }}>
<Row gutter={16}>
<Col span={6}>
<Input
placeholder="Name"
value={item.name}
onChange={(e) => handleUpdateCostItem(item.id, 'name', e.target.value)}
/>
</Col>
<Col span={4}>
<Select value={item.type} onChange={(v) => handleUpdateCostItem(item.id, 'type', v)}>
<Option value="FIXED">Fixed</Option>
<Option value="PERCENTAGE">Percentage</Option>
<Option value="PER_UNIT">Per Unit</Option>
</Select>
</Col>
<Col span={4}>
<InputNumber
style={{ width: '100%' }}
placeholder="Value"
value={item.value}
onChange={(v) => handleUpdateCostItem(item.id, 'value', v || 0)}
/>
</Col>
<Col span={4}>
<Select value={item.applyTo} onChange={(v) => handleUpdateCostItem(item.id, 'applyTo', v)}>
<Option value="PRODUCT">Product</Option>
<Option value="SHIPPING">Shipping</Option>
<Option value="ORDER">Order</Option>
<Option value="PLATFORM_FEE">Platform Fee</Option>
</Select>
</Col>
<Col span={4}>
<Button danger icon={<DeleteOutlined />} onClick={() => handleRemoveCostItem(item.id)} />
</Col>
</Row>
</Card>
))}
</Form>
</Modal>
<Modal
title={`Template: ${viewingTemplate?.name}`}
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={null}
width={700}
>
{viewingTemplate && (
<>
<Row gutter={16}>
<Col span={8}>
<p><strong>Platform:</strong> <Tag color="blue">{viewingTemplate.platform}</Tag></p>
</Col>
<Col span={8}>
<p><strong>Category:</strong> {viewingTemplate.category}</p>
</Col>
<Col span={8}>
<p><strong>Status:</strong> <Tag color={viewingTemplate.isActive ? 'success' : 'default'}>{viewingTemplate.isActive ? 'Active' : 'Inactive'}</Tag></p>
</Col>
</Row>
<p><strong>Description:</strong> {viewingTemplate.description}</p>
<Divider>Cost Items ({viewingTemplate.costs.length})</Divider>
<Table
dataSource={viewingTemplate.costs}
rowKey="id"
size="small"
pagination={false}
columns={[
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Type', dataIndex: 'type', key: 'type', render: (t) => <Tag color={COST_TYPE_CONFIG[t].color}>{COST_TYPE_CONFIG[t].text}</Tag> },
{ title: 'Value', dataIndex: 'value', key: 'value', render: (v, r) => r.type === 'PERCENTAGE' ? `${v}%` : `$${v}` },
{ title: 'Apply To', dataIndex: 'applyTo', key: 'applyTo' },
{ title: 'Description', dataIndex: 'description', key: 'description' },
]}
/>
</>
)}
</Modal>
</div>
);
};
export default CostTemplateConfig;

View File

@@ -0,0 +1,385 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
InputNumber,
Select,
Tag,
message,
Popconfirm,
Tooltip,
Row,
Col,
Statistic,
DatePicker,
Alert,
Divider,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SyncOutlined,
DollarOutlined,
HistoryOutlined,
LineChartOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import moment from 'moment';
import { settingsDataSource, ExchangeRate } from '@/services/settingsDataSource';
const { Option } = Select;
interface RateHistory {
id: string;
fromCurrency: string;
toCurrency: string;
rate: number;
recordedAt: string;
source: string;
}
const CURRENCY_LIST = [
{ code: 'USD', name: 'US Dollar', symbol: '$' },
{ code: 'CNY', name: 'Chinese Yuan', symbol: '¥' },
{ code: 'EUR', name: 'Euro', symbol: '€' },
{ code: 'GBP', name: 'British Pound', symbol: '£' },
{ code: 'JPY', name: 'Japanese Yen', symbol: '¥' },
{ code: 'AUD', name: 'Australian Dollar', symbol: 'A$' },
{ code: 'CAD', name: 'Canadian Dollar', symbol: 'C$' },
{ code: 'SGD', name: 'Singapore Dollar', symbol: 'S$' },
{ code: 'HKD', name: 'Hong Kong Dollar', symbol: 'HK$' },
{ code: 'KRW', name: 'Korean Won', symbol: '₩' },
];
const SOURCE_CONFIG: Record<string, { color: string; text: string }> = {
MANUAL: { color: 'blue', text: 'Manual' },
AUTO: { color: 'green', text: 'Auto' },
API: { color: 'purple', text: 'API' },
};
const ExchangeRateConfig: React.FC = () => {
const [loading, setLoading] = useState(false);
const [rates, setRates] = useState<ExchangeRate[]>([]);
const [history, setHistory] = useState<RateHistory[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [editingRate, setEditingRate] = useState<ExchangeRate | null>(null);
const [selectedPair, setSelectedPair] = useState<{ from: string; to: string } | null>(null);
const [form] = Form.useForm();
useEffect(() => {
fetchRates();
}, []);
const fetchRates = async () => {
setLoading(true);
try {
const mockRates: ExchangeRate[] = [
{ id: '1', fromCurrency: 'USD', toCurrency: 'CNY', rate: 7.24, source: 'AUTO', updatedAt: '2026-03-18 10:00:00', updatedBy: 'system', effectiveDate: '2026-03-18', isActive: true },
{ id: '2', fromCurrency: 'EUR', toCurrency: 'CNY', rate: 7.86, source: 'AUTO', updatedAt: '2026-03-18 10:00:00', updatedBy: 'system', effectiveDate: '2026-03-18', isActive: true },
{ id: '3', fromCurrency: 'GBP', toCurrency: 'CNY', rate: 9.21, source: 'AUTO', updatedAt: '2026-03-18 10:00:00', updatedBy: 'system', effectiveDate: '2026-03-18', isActive: true },
{ id: '4', fromCurrency: 'JPY', toCurrency: 'CNY', rate: 0.048, source: 'AUTO', updatedAt: '2026-03-18 10:00:00', updatedBy: 'system', effectiveDate: '2026-03-18', isActive: true },
{ id: '5', fromCurrency: 'USD', toCurrency: 'EUR', rate: 0.92, source: 'MANUAL', updatedAt: '2026-03-17 15:00:00', updatedBy: 'admin', effectiveDate: '2026-03-17', isActive: true },
{ id: '6', fromCurrency: 'AUD', toCurrency: 'CNY', rate: 4.72, source: 'API', updatedAt: '2026-03-18 09:30:00', updatedBy: 'system', effectiveDate: '2026-03-18', isActive: true },
];
setRates(mockRates);
} catch (error) {
message.error('Failed to load exchange rates');
} finally {
setLoading(false);
}
};
const fetchHistory = (from: string, to: string) => {
const mockHistory: RateHistory[] = [
{ id: '1', fromCurrency: from, toCurrency: to, rate: 7.24, recordedAt: '2026-03-18 10:00:00', source: 'AUTO' },
{ id: '2', fromCurrency: from, toCurrency: to, rate: 7.22, recordedAt: '2026-03-17 10:00:00', source: 'AUTO' },
{ id: '3', fromCurrency: from, toCurrency: to, rate: 7.20, recordedAt: '2026-03-16 10:00:00', source: 'AUTO' },
{ id: '4', fromCurrency: from, toCurrency: to, rate: 7.18, recordedAt: '2026-03-15 10:00:00', source: 'AUTO' },
{ id: '5', fromCurrency: from, toCurrency: to, rate: 7.15, recordedAt: '2026-03-14 10:00:00', source: 'AUTO' },
];
setHistory(mockHistory);
setSelectedPair({ from, to });
setHistoryModalVisible(true);
};
const handleAdd = () => {
setEditingRate(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (record: ExchangeRate) => {
setEditingRate(record);
form.setFieldsValue({
...record,
effectiveDate: moment(record.effectiveDate),
expiryDate: record.expiryDate ? moment(record.expiryDate) : undefined,
});
setModalVisible(true);
};
const handleDelete = (id: string) => {
setRates(rates.filter(r => r.id !== id));
message.success('Exchange rate deleted successfully');
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const rateData = {
...values,
effectiveDate: values.effectiveDate.format('YYYY-MM-DD'),
expiryDate: values.expiryDate?.format('YYYY-MM-DD'),
updatedAt: moment().format('YYYY-MM-DD HH:mm:ss'),
updatedBy: 'admin',
source: 'MANUAL' as const,
isActive: true,
};
if (editingRate) {
setRates(rates.map(r => r.id === editingRate.id ? { ...r, ...rateData } : r));
message.success('Exchange rate updated successfully');
} else {
setRates([...rates, { ...rateData, id: `${Date.now()}` }]);
message.success('Exchange rate added successfully');
}
setModalVisible(false);
} catch (error) {
console.error('Form validation failed:', error);
}
};
const handleSyncFromAPI = async () => {
message.loading('Syncing exchange rates from API...', 2);
await new Promise(resolve => setTimeout(resolve, 2000));
message.success('Exchange rates synced successfully');
fetchRates();
};
const handleToggleActive = (id: string, checked: boolean) => {
setRates(rates.map(r => r.id === id ? { ...r, isActive: checked } : r));
message.success(`Exchange rate ${checked ? 'activated' : 'deactivated'}`);
};
const stats = {
total: rates.length,
active: rates.filter(r => r.isActive).length,
manual: rates.filter(r => r.source === 'MANUAL').length,
auto: rates.filter(r => r.source === 'AUTO' || r.source === 'API').length,
};
const columns: ColumnsType<ExchangeRate> = [
{
title: 'From',
dataIndex: 'fromCurrency',
key: 'fromCurrency',
width: 100,
render: (code) => {
const currency = CURRENCY_LIST.find(c => c.code === code);
return <Tag>{code} ({currency?.symbol})</Tag>;
},
},
{
title: 'To',
dataIndex: 'toCurrency',
key: 'toCurrency',
width: 100,
render: (code) => {
const currency = CURRENCY_LIST.find(c => c.code === code);
return <Tag color="blue">{code} ({currency?.symbol})</Tag>;
},
},
{
title: 'Rate',
dataIndex: 'rate',
key: 'rate',
width: 120,
render: (rate) => <span style={{ fontWeight: 'bold', fontSize: 16 }}>{rate.toFixed(4)}</span>,
},
{
title: 'Source',
dataIndex: 'source',
key: 'source',
width: 100,
render: (source) => {
const config = SOURCE_CONFIG[source];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Effective Date',
dataIndex: 'effectiveDate',
key: 'effectiveDate',
width: 120,
},
{
title: 'Updated',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 160,
},
{
title: 'Active',
dataIndex: 'isActive',
key: 'isActive',
width: 80,
render: (active, record) => (
<Tag color={active ? 'success' : 'default'} onClick={() => handleToggleActive(record.id, !active)} style={{ cursor: 'pointer' }}>
{active ? 'Yes' : 'No'}
</Tag>
),
},
{
title: 'Actions',
key: 'action',
width: 150,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Tooltip title="Edit">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</Tooltip>
<Tooltip title="History">
<Button type="link" size="small" icon={<HistoryOutlined />} onClick={() => fetchHistory(record.fromCurrency, record.toCurrency)} />
</Tooltip>
<Popconfirm title="Delete this rate?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
const historyColumns: ColumnsType<RateHistory> = [
{ title: 'Date', dataIndex: 'recordedAt', key: 'recordedAt', width: 180 },
{ title: 'Rate', dataIndex: 'rate', key: 'rate', render: (r) => r.toFixed(4) },
{ title: 'Source', dataIndex: 'source', key: 'source' },
];
return (
<div className="exchange-rate-config">
<Alert
message="Exchange rates are automatically synced daily at 10:00 AM. You can also manually add or edit rates."
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="Total Pairs" value={stats.total} prefix={<DollarOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Active" value={stats.active} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Manual" value={stats.manual} valueStyle={{ color: '#1890ff' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Auto/API" value={stats.auto} valueStyle={{ color: '#722ed1' }} />
</Card>
</Col>
</Row>
<Card
title="Exchange Rate Configuration"
extra={
<Space>
<Button icon={<SyncOutlined />} onClick={handleSyncFromAPI}>
Sync from API
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
Add Rate
</Button>
</Space>
}
>
<Table
columns={columns}
dataSource={rates}
rowKey="id"
loading={loading}
scroll={{ x: 1000 }}
pagination={{ showSizeChanger: true, showTotal: (total) => `Total ${total} rates` }}
/>
</Card>
<Modal
title={editingRate ? 'Edit Exchange Rate' : 'Add Exchange Rate'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={500}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="fromCurrency" label="From Currency" rules={[{ required: true }]}>
<Select placeholder="Select currency" showSearch optionFilterProp="children">
{CURRENCY_LIST.map(c => (
<Option key={c.code} value={c.code}>{c.code} - {c.name}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="toCurrency" label="To Currency" rules={[{ required: true }]}>
<Select placeholder="Select currency" showSearch optionFilterProp="children">
{CURRENCY_LIST.map(c => (
<Option key={c.code} value={c.code}>{c.code} - {c.name}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="rate" label="Exchange Rate" rules={[{ required: true }]}>
<InputNumber style={{ width: '100%' }} min={0} step={0.0001} precision={4} placeholder="Enter rate" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="effectiveDate" label="Effective Date" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="expiryDate" label="Expiry Date">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title={`Rate History: ${selectedPair?.from}/${selectedPair?.to}`}
open={historyModalVisible}
onCancel={() => setHistoryModalVisible(false)}
footer={null}
width={600}
>
<Table
columns={historyColumns}
dataSource={history}
rowKey="id"
pagination={false}
size="small"
/>
</Modal>
</div>
);
};
export default ExchangeRateConfig;

View File

@@ -0,0 +1,446 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
Switch,
Tag,
message,
Popconfirm,
Tooltip,
Row,
Col,
Statistic,
Badge,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
ApiOutlined,
ShopOutlined,
LinkOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { settingsDataSource, PlatformAccount } from '@/services/settingsDataSource';
const { Option } = Select;
const { Password } = Input;
const PLATFORM_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
AMAZON: { color: 'orange', text: 'Amazon', icon: <ShopOutlined /> },
EBAY: { color: 'blue', text: 'eBay', icon: <ShopOutlined /> },
SHOPIFY: { color: 'green', text: 'Shopify', icon: <ShopOutlined /> },
SHOPEE: { color: 'red', text: 'Shopee', icon: <ShopOutlined /> },
LAZADA: { color: 'purple', text: 'Lazada', icon: <ShopOutlined /> },
ALIBABA: { color: 'orange', text: 'Alibaba', icon: <ShopOutlined /> },
};
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
ACTIVE: { color: 'success', text: 'Active' },
INACTIVE: { color: 'default', text: 'Inactive' },
EXPIRED: { color: 'warning', text: 'Expired' },
ERROR: { color: 'error', text: 'Error' },
};
const PlatformAccountConfig: React.FC = () => {
const [loading, setLoading] = useState(false);
const [accounts, setAccounts] = useState<PlatformAccount[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [editingAccount, setEditingAccount] = useState<PlatformAccount | null>(null);
const [testLoading, setTestLoading] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
fetchAccounts();
}, []);
const fetchAccounts = async () => {
setLoading(true);
try {
const mockAccounts: PlatformAccount[] = [
{
id: '1',
platform: 'AMAZON',
accountName: 'US Store',
shopId: 'AMZ-US-001',
shopName: 'Amazon US Store',
region: 'US',
status: 'ACTIVE',
apiConnected: true,
lastSync: '2026-03-18 10:30:00',
tokenExpiry: '2026-06-18',
autoSync: true,
syncInterval: 30,
},
{
id: '2',
platform: 'EBAY',
accountName: 'eBay Outlet',
shopId: 'EB-001',
shopName: 'eBay Outlet Store',
region: 'US',
status: 'ACTIVE',
apiConnected: true,
lastSync: '2026-03-18 09:15:00',
tokenExpiry: '2026-05-15',
autoSync: true,
syncInterval: 60,
},
{
id: '3',
platform: 'SHOPIFY',
accountName: 'Brand Store',
shopId: 'SH-BRAND-001',
shopName: 'Shopify Brand Store',
region: 'US',
status: 'ACTIVE',
apiConnected: true,
lastSync: '2026-03-18 08:00:00',
tokenExpiry: '2027-03-18',
autoSync: false,
syncInterval: 120,
},
{
id: '4',
platform: 'SHOPEE',
accountName: 'Shopee Mall',
shopId: 'SP-MALL-001',
shopName: 'Shopee Mall Store',
region: 'SG',
status: 'EXPIRED',
apiConnected: false,
lastSync: '2026-03-10 14:00:00',
tokenExpiry: '2026-03-15',
autoSync: true,
syncInterval: 30,
},
];
setAccounts(mockAccounts);
} catch (error) {
message.error('Failed to load platform accounts');
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingAccount(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (record: PlatformAccount) => {
setEditingAccount(record);
form.setFieldsValue(record);
setModalVisible(true);
};
const handleDelete = (id: string) => {
setAccounts(accounts.filter(a => a.id !== id));
message.success('Account deleted successfully');
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingAccount) {
setAccounts(accounts.map(a =>
a.id === editingAccount.id ? { ...a, ...values } : a
));
message.success('Account updated successfully');
} else {
const newAccount: PlatformAccount = {
id: `${Date.now()}`,
...values,
status: 'INACTIVE',
apiConnected: false,
lastSync: '-',
tokenExpiry: '-',
};
setAccounts([...accounts, newAccount]);
message.success('Account added successfully');
}
setModalVisible(false);
} catch (error) {
console.error('Form validation failed:', error);
}
};
const handleTestConnection = async () => {
setTestLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1500));
message.success('API connection test successful');
} catch (error) {
message.error('API connection test failed');
} finally {
setTestLoading(false);
}
};
const handleSync = async (id: string) => {
message.loading('Starting sync...', 1);
await new Promise(resolve => setTimeout(resolve, 1000));
setAccounts(accounts.map(a =>
a.id === id ? { ...a, lastSync: new Date().toISOString().replace('T', ' ').slice(0, 19) } : a
));
message.success('Sync completed');
};
const handleToggleAutoSync = (id: string, checked: boolean) => {
setAccounts(accounts.map(a =>
a.id === id ? { ...a, autoSync: checked } : a
));
message.success(`Auto sync ${checked ? 'enabled' : 'disabled'}`);
};
const stats = {
total: accounts.length,
active: accounts.filter(a => a.status === 'ACTIVE').length,
expired: accounts.filter(a => a.status === 'EXPIRED').length,
error: accounts.filter(a => a.status === 'ERROR').length,
};
const columns: ColumnsType<PlatformAccount> = [
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 120,
render: (platform) => {
const config = PLATFORM_CONFIG[platform];
return (
<Tag color={config.color} icon={config.icon}>
{config.text}
</Tag>
);
},
},
{
title: 'Account Name',
dataIndex: 'accountName',
key: 'accountName',
width: 150,
},
{
title: 'Shop ID',
dataIndex: 'shopId',
key: 'shopId',
width: 130,
},
{
title: 'Region',
dataIndex: 'region',
key: 'region',
width: 80,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status) => {
const config = STATUS_CONFIG[status];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'API',
dataIndex: 'apiConnected',
key: 'apiConnected',
width: 80,
render: (connected) => (
<Tag color={connected ? 'success' : 'error'} icon={connected ? <CheckCircleOutlined /> : <CloseCircleOutlined />}>
{connected ? 'Connected' : 'Disconnected'}
</Tag>
),
},
{
title: 'Last Sync',
dataIndex: 'lastSync',
key: 'lastSync',
width: 160,
},
{
title: 'Token Expiry',
dataIndex: 'tokenExpiry',
key: 'tokenExpiry',
width: 120,
render: (date) => {
const isExpiringSoon = date !== '-' && new Date(date) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
return <span style={{ color: isExpiringSoon ? '#ff4d4f' : undefined }}>{date}</span>;
},
},
{
title: 'Auto Sync',
dataIndex: 'autoSync',
key: 'autoSync',
width: 100,
render: (checked, record) => (
<Switch checked={checked} onChange={(c) => handleToggleAutoSync(record.id, c)} size="small" />
),
},
{
title: 'Actions',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Tooltip title="Edit">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</Tooltip>
<Tooltip title="Sync Now">
<Button type="link" size="small" icon={<SyncOutlined />} onClick={() => handleSync(record.id)} />
</Tooltip>
<Popconfirm
title="Delete this account?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div className="platform-account-config">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="Total Accounts" value={stats.total} prefix={<ApiOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Active" value={stats.active} valueStyle={{ color: '#52c41a' }} prefix={<CheckCircleOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Expired" value={stats.expired} valueStyle={{ color: '#faad14' }} prefix={<CloseCircleOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Error" value={stats.error} valueStyle={{ color: '#ff4d4f' }} />
</Card>
</Col>
</Row>
<Card
title="Platform Account Configuration"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
Add Account
</Button>
}
>
<Table
columns={columns}
dataSource={accounts}
rowKey="id"
loading={loading}
scroll={{ x: 1200 }}
pagination={{ showSizeChanger: true, showTotal: (total) => `Total ${total} accounts` }}
/>
</Card>
<Modal
title={editingAccount ? 'Edit Account' : 'Add Account'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={600}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
<Select placeholder="Select platform">
{Object.entries(PLATFORM_CONFIG).map(([key, config]) => (
<Option key={key} value={key}>{config.text}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="accountName" label="Account Name" rules={[{ required: true }]}>
<Input placeholder="Enter account name" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="shopId" label="Shop ID" rules={[{ required: true }]}>
<Input placeholder="Enter shop ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="shopName" label="Shop Name" rules={[{ required: true }]}>
<Input placeholder="Enter shop name" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="region" label="Region" rules={[{ required: true }]}>
<Select placeholder="Select region">
<Option value="US">United States</Option>
<Option value="UK">United Kingdom</Option>
<Option value="EU">Europe</Option>
<Option value="CN">China</Option>
<Option value="SG">Singapore</Option>
<Option value="JP">Japan</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="syncInterval" label="Sync Interval (minutes)">
<Select placeholder="Select interval">
<Option value={15}>15 minutes</Option>
<Option value={30}>30 minutes</Option>
<Option value={60}>1 hour</Option>
<Option value={120}>2 hours</Option>
<Option value={360}>6 hours</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="autoSync" label="Auto Sync" valuePropName="checked">
<Switch />
</Form.Item>
<Card size="small" title="API Credentials" style={{ marginBottom: 16 }}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="apiKey" label="API Key">
<Input placeholder="Enter API key" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="apiSecret" label="API Secret">
<Password placeholder="Enter API secret" />
</Form.Item>
</Col>
</Row>
<Button icon={<LinkOutlined />} loading={testLoading} onClick={handleTestConnection}>
Test Connection
</Button>
</Card>
</Form>
</Modal>
</div>
);
};
export default PlatformAccountConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,11 @@ import React, { useState, useEffect } from 'react';
import { Table, Button, Input, Select, message, Card, Modal, Form } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { settingsDataSource, User } from '@/services/settingsDataSource';
const { Option } = Select;
const { Search } = Input;
interface User {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
createdAt: string;
lastLogin: string;
}
const UserManagement: React.FC = () => {
const navigate = useNavigate();
const [users, setUsers] = useState<User[]>([]);
@@ -34,30 +25,15 @@ const UserManagement: React.FC = () => {
const fetchUsers = async () => {
setLoading(true);
// 模拟API调用
setTimeout(() => {
const mockUsers: User[] = [
{
id: '1',
name: 'John Doe',
email: 'john.doe@example.com',
role: 'ADMIN',
status: 'active',
createdAt: '2026-01-01',
lastLogin: '2026-03-17',
},
{
id: '2',
name: 'Jane Smith',
email: 'jane.smith@example.com',
role: 'MANAGER',
status: 'active',
createdAt: '2026-01-02',
lastLogin: '2026-03-16',
},
{
id: '3',
name: 'Bob Johnson',
try {
const data = await settingsDataSource.fetchUsers();
setUsers(data);
} catch (error) {
message.error('Failed to load users');
} finally {
setLoading(false);
}
};
email: 'bob.johnson@example.com',
role: 'OPERATOR',
status: 'active',

View File

@@ -0,0 +1,531 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
Switch,
Tag,
message,
Popconfirm,
Tooltip,
Row,
Col,
Statistic,
Badge,
Progress,
Descriptions,
Divider,
Alert,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
DesktopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
SettingOutlined,
GlobalOutlined,
SafetyOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { settingsDataSource, WinNode } from '@/services/settingsDataSource';
const { Option } = Select;
const { Password } = Input;
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
ONLINE: { color: 'success', text: 'Online', icon: <CheckCircleOutlined /> },
OFFLINE: { color: 'default', text: 'Offline', icon: <CloseCircleOutlined /> },
BUSY: { color: 'processing', text: 'Busy', icon: <SyncOutlined spin /> },
ERROR: { color: 'error', text: 'Error', icon: <CloseCircleOutlined /> },
};
const FINGERPRINT_POLICIES = [
'STANDARD',
'STEALTH',
'RANDOM',
'CUSTOM',
];
const WinNodeConfig: React.FC = () => {
const [loading, setLoading] = useState(false);
const [nodes, setNodes] = useState<WinNode[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [editingNode, setEditingNode] = useState<WinNode | null>(null);
const [viewingNode, setViewingNode] = useState<WinNode | null>(null);
const [form] = Form.useForm();
useEffect(() => {
fetchNodes();
const interval = setInterval(fetchNodes, 30000);
return () => clearInterval(interval);
}, []);
const fetchNodes = async () => {
setLoading(true);
try {
const mockNodes: WinNode[] = [
{
id: '1',
name: 'WinNode-US-01',
host: '192.168.1.101',
port: 9222,
status: 'ONLINE',
shopId: 'SHOP_001',
shopName: 'Amazon US Store',
profileDir: 'C:\\Profiles\\AMZ_US_01',
proxy: 'us-proxy.example.com:8080',
fingerprintPolicy: 'STEALTH',
maxConcurrent: 3,
currentTasks: 1,
cpuUsage: 35,
memoryUsage: 42,
lastHeartbeat: '2026-03-18 10:30:00',
autoRestart: true,
},
{
id: '2',
name: 'WinNode-US-02',
host: '192.168.1.102',
port: 9222,
status: 'BUSY',
shopId: 'SHOP_001',
shopName: 'Amazon US Store',
profileDir: 'C:\\Profiles\\AMZ_US_02',
proxy: 'us-proxy.example.com:8080',
fingerprintPolicy: 'STEALTH',
maxConcurrent: 3,
currentTasks: 3,
cpuUsage: 78,
memoryUsage: 65,
lastHeartbeat: '2026-03-18 10:30:05',
autoRestart: true,
},
{
id: '3',
name: 'WinNode-EU-01',
host: '192.168.1.103',
port: 9222,
status: 'ONLINE',
shopId: 'SHOP_002',
shopName: 'eBay Outlet',
profileDir: 'C:\\Profiles\\EB_EU_01',
proxy: 'eu-proxy.example.com:8080',
fingerprintPolicy: 'STANDARD',
maxConcurrent: 2,
currentTasks: 0,
cpuUsage: 12,
memoryUsage: 28,
lastHeartbeat: '2026-03-18 10:30:02',
autoRestart: false,
},
{
id: '4',
name: 'WinNode-ASIA-01',
host: '192.168.1.104',
port: 9222,
status: 'OFFLINE',
shopId: 'SHOP_003',
shopName: 'Shopee Mall',
profileDir: 'C:\\Profiles\\SP_ASIA_01',
proxy: 'sg-proxy.example.com:8080',
fingerprintPolicy: 'RANDOM',
maxConcurrent: 2,
currentTasks: 0,
cpuUsage: 0,
memoryUsage: 0,
lastHeartbeat: '2026-03-17 18:00:00',
autoRestart: true,
},
{
id: '5',
name: 'WinNode-US-03',
host: '192.168.1.105',
port: 9222,
status: 'ERROR',
shopId: 'SHOP_004',
shopName: 'Shopify Brand',
profileDir: 'C:\\Profiles\\SF_US_01',
proxy: 'us-proxy2.example.com:8080',
fingerprintPolicy: 'STEALTH',
maxConcurrent: 3,
currentTasks: 0,
cpuUsage: 0,
memoryUsage: 0,
lastHeartbeat: '2026-03-18 08:15:00',
autoRestart: true,
},
];
setNodes(mockNodes);
} catch (error) {
message.error('Failed to load WinNodes');
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingNode(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (record: WinNode) => {
setEditingNode(record);
form.setFieldsValue(record);
setModalVisible(true);
};
const handleView = (record: WinNode) => {
setViewingNode(record);
setDetailModalVisible(true);
};
const handleDelete = (id: string) => {
setNodes(nodes.filter(n => n.id !== id));
message.success('WinNode deleted successfully');
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingNode) {
setNodes(nodes.map(n => n.id === editingNode.id ? { ...n, ...values } : n));
message.success('WinNode updated successfully');
} else {
const newNode: WinNode = {
...values,
id: `${Date.now()}`,
status: 'OFFLINE',
currentTasks: 0,
cpuUsage: 0,
memoryUsage: 0,
lastHeartbeat: '-',
};
setNodes([...nodes, newNode]);
message.success('WinNode added successfully');
}
setModalVisible(false);
} catch (error) {
console.error('Form validation failed:', error);
}
};
const handleRestart = async (id: string) => {
message.loading('Restarting node...', 2);
await new Promise(resolve => setTimeout(resolve, 2000));
setNodes(nodes.map(n =>
n.id === id ? { ...n, status: 'ONLINE', lastHeartbeat: new Date().toISOString().replace('T', ' ').slice(0, 19) } : n
));
message.success('Node restarted successfully');
};
const handleTestConnection = async (id: string) => {
message.loading('Testing connection...', 1.5);
await new Promise(resolve => setTimeout(resolve, 1500));
message.success('Connection test successful');
};
const handleToggleAutoRestart = (id: string, checked: boolean) => {
setNodes(nodes.map(n => n.id === id ? { ...n, autoRestart: checked } : n));
message.success(`Auto restart ${checked ? 'enabled' : 'disabled'}`);
};
const stats = {
total: nodes.length,
online: nodes.filter(n => n.status === 'ONLINE').length,
busy: nodes.filter(n => n.status === 'BUSY').length,
offline: nodes.filter(n => n.status === 'OFFLINE' || n.status === 'ERROR').length,
totalTasks: nodes.reduce((sum, n) => sum + n.currentTasks, 0),
};
const columns: ColumnsType<WinNode> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 150,
render: (text, record) => (
<a onClick={() => handleView(record)}>
<DesktopOutlined style={{ marginRight: 8 }} />
{text}
</a>
),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status) => {
const config = STATUS_CONFIG[status];
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
},
},
{
title: 'Shop',
dataIndex: 'shopName',
key: 'shopName',
width: 150,
},
{
title: 'Host:Port',
key: 'hostPort',
width: 150,
render: (_, record) => `${record.host}:${record.port}`,
},
{
title: 'Tasks',
key: 'tasks',
width: 100,
render: (_, record) => (
<Badge
count={`${record.currentTasks}/${record.maxConcurrent}`}
style={{ backgroundColor: record.currentTasks >= record.maxConcurrent ? '#ff4d4f' : '#1890ff' }}
/>
),
},
{
title: 'CPU',
dataIndex: 'cpuUsage',
key: 'cpuUsage',
width: 100,
render: (usage) => (
<Progress
percent={usage}
size="small"
status={usage > 80 ? 'exception' : 'active'}
format={(p) => `${p}%`}
/>
),
},
{
title: 'Memory',
dataIndex: 'memoryUsage',
key: 'memoryUsage',
width: 100,
render: (usage) => (
<Progress
percent={usage}
size="small"
status={usage > 80 ? 'exception' : 'active'}
format={(p) => `${p}%`}
/>
),
},
{
title: 'Last Heartbeat',
dataIndex: 'lastHeartbeat',
key: 'lastHeartbeat',
width: 160,
},
{
title: 'Actions',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Tooltip title="View">
<Button type="link" size="small" onClick={() => handleView(record)}>View</Button>
</Tooltip>
<Tooltip title="Edit">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</Tooltip>
<Tooltip title="Restart">
<Button type="link" size="small" icon={<SyncOutlined />} onClick={() => handleRestart(record.id)} />
</Tooltip>
<Popconfirm title="Delete this node?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div className="winnode-config">
<Alert
message="WinNodes are Windows-based execution nodes for browser automation tasks. Each node is isolated per shop with unique profile and proxy settings."
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic title="Total Nodes" value={stats.total} prefix={<DesktopOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Online" value={stats.online} valueStyle={{ color: '#52c41a' }} prefix={<CheckCircleOutlined />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Busy" value={stats.busy} valueStyle={{ color: '#1890ff' }} prefix={<SyncOutlined spin />} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="Offline/Error" value={stats.offline} valueStyle={{ color: '#ff4d4f' }} prefix={<CloseCircleOutlined />} />
</Card>
</Col>
</Row>
<Card
title="WinNode Configuration"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
Add Node
</Button>
}
>
<Table
columns={columns}
dataSource={nodes}
rowKey="id"
loading={loading}
scroll={{ x: 1200 }}
pagination={{ showSizeChanger: true, showTotal: (total) => `Total ${total} nodes` }}
/>
</Card>
<Modal
title={editingNode ? 'Edit WinNode' : 'Add WinNode'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={700}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="Node Name" rules={[{ required: true }]}>
<Input placeholder="Enter node name" />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="host" label="Host" rules={[{ required: true }]}>
<Input placeholder="IP address" />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="port" label="Port" rules={[{ required: true }]}>
<Input type="number" placeholder="Port" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="shopId" label="Shop ID" rules={[{ required: true }]}>
<Input placeholder="Enter shop ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="shopName" label="Shop Name" rules={[{ required: true }]}>
<Input placeholder="Enter shop name" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="profileDir" label="Profile Directory" rules={[{ required: true }]}>
<Input placeholder="C:\Profiles\..." />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="proxy" label="Proxy" rules={[{ required: true }]}>
<Input placeholder="proxy.example.com:8080" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="fingerprintPolicy" label="Fingerprint Policy" rules={[{ required: true }]}>
<Select placeholder="Select policy">
{FINGERPRINT_POLICIES.map(p => (
<Option key={p} value={p}>{p}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="maxConcurrent" label="Max Concurrent Tasks" rules={[{ required: true }]}>
<Input type="number" min={1} max={10} placeholder="1-10" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="autoRestart" label="Auto Restart" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title={`WinNode: ${viewingNode?.name}`}
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="test" onClick={() => viewingNode && handleTestConnection(viewingNode.id)}>
Test Connection
</Button>,
<Button key="restart" type="primary" onClick={() => viewingNode && handleRestart(viewingNode.id)}>
Restart
</Button>,
]}
width={700}
>
{viewingNode && (
<>
<Descriptions bordered column={2}>
<Descriptions.Item label="Status">
<Tag color={STATUS_CONFIG[viewingNode.status].color} icon={STATUS_CONFIG[viewingNode.status].icon}>
{STATUS_CONFIG[viewingNode.status].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Host:Port">{viewingNode.host}:{viewingNode.port}</Descriptions.Item>
<Descriptions.Item label="Shop">{viewingNode.shopName}</Descriptions.Item>
<Descriptions.Item label="Shop ID">{viewingNode.shopId}</Descriptions.Item>
<Descriptions.Item label="Profile Dir" span={2}>{viewingNode.profileDir}</Descriptions.Item>
<Descriptions.Item label="Proxy">{viewingNode.proxy}</Descriptions.Item>
<Descriptions.Item label="Fingerprint Policy">{viewingNode.fingerprintPolicy}</Descriptions.Item>
<Descriptions.Item label="Max Concurrent">{viewingNode.maxConcurrent}</Descriptions.Item>
<Descriptions.Item label="Current Tasks">{viewingNode.currentTasks}</Descriptions.Item>
<Descriptions.Item label="Auto Restart">
<Switch checked={viewingNode.autoRestart} onChange={(c) => handleToggleAutoRestart(viewingNode.id, c)} size="small" />
</Descriptions.Item>
<Descriptions.Item label="Last Heartbeat">{viewingNode.lastHeartbeat}</Descriptions.Item>
</Descriptions>
<Divider>Resource Usage</Divider>
<Row gutter={16}>
<Col span={12}>
<p>CPU Usage</p>
<Progress percent={viewingNode.cpuUsage} status={viewingNode.cpuUsage > 80 ? 'exception' : 'active'} />
</Col>
<Col span={12}>
<p>Memory Usage</p>
<Progress percent={viewingNode.memoryUsage} status={viewingNode.memoryUsage > 80 ? 'exception' : 'active'} />
</Col>
</Row>
</>
)}
</Modal>
</div>
);
};
export default WinNodeConfig;

File diff suppressed because it is too large Load Diff