feat: 实现前端组件库和API服务基础架构

refactor: 移除废弃的AGI策略演进服务

fix: 修正磁盘I/O指标字段命名

chore: 更新项目依赖版本

test: 添加前后端集成测试用例

docs: 更新AI模块接口文档

style: 统一审计日志字段命名规范

perf: 优化Redis订阅连接错误处理

build: 配置多项目工作区结构

ci: 添加Vite开发服务器CORS支持
This commit is contained in:
2026-03-18 15:22:55 +08:00
parent b31591e04c
commit c932a67be2
96 changed files with 37748 additions and 16326 deletions

View File

@@ -0,0 +1,2 @@
export { default as BlacklistManage } from './BlacklistManage';
export { default as RiskMonitor } from './RiskMonitor';

View File

@@ -0,0 +1,541 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
DatePicker,
InputNumber,
Descriptions,
Divider,
message,
Tag,
Tabs,
Row,
Col,
Statistic,
Alert,
Tooltip,
Badge,
Spin,
} from 'antd';
import {
BarChartOutlined,
LineChartOutlined,
WarningOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined,
DownloadOutlined,
FilterOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer } from 'recharts';
const { Option } = Select;
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
interface ReturnData {
id: string;
tenant_id: string;
shop_id: string;
sku_id: string;
sku_name: string;
product_name: string;
return_rate: number;
total_orders: number;
total_returns: number;
return_amount: number;
avg_processing_time: number;
return_reasons: string[];
status: 'NORMAL' | 'WARNING' | 'HIGH_RISK';
last_return_date: string;
created_at: string;
updated_at: string;
}
interface ReturnTrend {
date: string;
return_rate: number;
order_count: number;
return_count: number;
}
const RETURN_STATUS: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
NORMAL: { color: 'green', text: '正常', icon: <CheckCircleOutlined /> },
WARNING: { color: 'orange', text: '警告', icon: <WarningOutlined /> },
HIGH_RISK: { color: 'red', text: '高风险', icon: <ExclamationCircleOutlined /> },
};
const RETURN_REASONS = [
'产品质量问题',
'尺寸不符',
'描述不符',
'物流问题',
'客户改变主意',
'重复订单',
'其他原因',
];
const PLATFORMS = [
'Amazon',
'eBay',
'Shopify',
'Walmart',
'TikTok Shop',
'Temu',
'Alibaba',
'Other',
];
const MOCK_RETURN_DATA: ReturnData[] = [
{
id: '1',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-001',
sku_name: 'Wireless Bluetooth Headphones',
product_name: 'Premium Wireless Headphones',
return_rate: 0.35,
total_orders: 120,
total_returns: 42,
return_amount: 8400.00,
avg_processing_time: 3.5,
return_reasons: ['产品质量问题', '描述不符'],
status: 'HIGH_RISK',
last_return_date: '2026-03-17',
created_at: '2026-03-01',
updated_at: '2026-03-17',
},
{
id: '2',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-002',
sku_name: 'Smart Fitness Tracker',
product_name: 'Fitness Tracker Pro',
return_rate: 0.15,
total_orders: 85,
total_returns: 13,
return_amount: 2600.00,
avg_processing_time: 2.8,
return_reasons: ['尺寸不符', '客户改变主意'],
status: 'WARNING',
last_return_date: '2026-03-16',
created_at: '2026-03-01',
updated_at: '2026-03-16',
},
{
id: '3',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-003',
sku_name: 'Portable Power Bank',
product_name: '10000mAh Power Bank',
return_rate: 0.08,
total_orders: 200,
total_returns: 16,
return_amount: 1600.00,
avg_processing_time: 2.2,
return_reasons: ['物流问题', '其他原因'],
status: 'NORMAL',
last_return_date: '2026-03-15',
created_at: '2026-03-01',
updated_at: '2026-03-15',
},
{
id: '4',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-004',
sku_name: 'Bluetooth Speaker',
product_name: 'Portable Bluetooth Speaker',
return_rate: 0.22,
total_orders: 95,
total_returns: 21,
return_amount: 4200.00,
avg_processing_time: 3.1,
return_reasons: ['产品质量问题', '描述不符', '客户改变主意'],
status: 'WARNING',
last_return_date: '2026-03-17',
created_at: '2026-03-01',
updated_at: '2026-03-17',
},
{
id: '5',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-005',
sku_name: 'USB-C Cable',
product_name: 'Fast Charging USB-C Cable',
return_rate: 0.05,
total_orders: 300,
total_returns: 15,
return_amount: 750.00,
avg_processing_time: 1.8,
return_reasons: ['物流问题', '其他原因'],
status: 'NORMAL',
last_return_date: '2026-03-14',
created_at: '2026-03-01',
updated_at: '2026-03-14',
},
];
const MOCK_RETURN_TRENDS: ReturnTrend[] = [
{ date: '2026-03-01', return_rate: 0.12, order_count: 45, return_count: 5 },
{ date: '2026-03-02', return_rate: 0.15, order_count: 52, return_count: 8 },
{ date: '2026-03-03', return_rate: 0.18, order_count: 48, return_count: 9 },
{ date: '2026-03-04', return_rate: 0.14, order_count: 55, return_count: 8 },
{ date: '2026-03-05', return_rate: 0.20, order_count: 42, return_count: 8 },
{ date: '2026-03-06', return_rate: 0.22, order_count: 38, return_count: 8 },
{ date: '2026-03-07', return_rate: 0.19, order_count: 40, return_count: 8 },
{ date: '2026-03-08', return_rate: 0.25, order_count: 45, return_count: 11 },
{ date: '2026-03-09', return_rate: 0.28, order_count: 50, return_count: 14 },
{ date: '2026-03-10', return_rate: 0.30, order_count: 48, return_count: 14 },
{ date: '2026-03-11', return_rate: 0.27, order_count: 52, return_count: 14 },
{ date: '2026-03-12', return_rate: 0.32, order_count: 45, return_count: 14 },
{ date: '2026-03-13', return_rate: 0.35, order_count: 40, return_count: 14 },
{ date: '2026-03-14', return_rate: 0.33, order_count: 42, return_count: 14 },
{ date: '2026-03-15', return_rate: 0.30, order_count: 48, return_count: 14 },
{ date: '2026-03-16', return_rate: 0.32, order_count: 50, return_count: 16 },
{ date: '2026-03-17', return_rate: 0.35, order_count: 55, return_count: 19 },
];
const ReturnMonitor: React.FC = () => {
const [returnData, setReturnData] = useState<ReturnData[]>(MOCK_RETURN_DATA);
const [loading, setLoading] = useState(false);
const [selectedRecord, setSelectedRecord] = useState<ReturnData | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [filters, setFilters] = useState({
platform: '',
status: '',
dateRange: null as any,
minReturnRate: 0,
maxReturnRate: 100,
});
const columns: ColumnsType<ReturnData> = [
{
title: 'SKU ID',
dataIndex: 'sku_id',
key: 'sku_id',
width: 120,
},
{
title: 'SKU Name',
dataIndex: 'sku_name',
key: 'sku_name',
width: 200,
render: (name: string, record: ReturnData) => (
<a onClick={() => handleViewDetail(record)}>{name}</a>
),
},
{
title: 'Product Name',
dataIndex: 'product_name',
key: 'product_name',
width: 200,
},
{
title: 'Return Rate',
dataIndex: 'return_rate',
key: 'return_rate',
width: 120,
render: (rate: number) => (
<div>
<span style={{ color: rate > 0.2 ? 'red' : rate > 0.1 ? 'orange' : 'green' }}>
{((rate) * 100).toFixed(1)}%
</span>
</div>
),
},
{
title: 'Total Orders',
dataIndex: 'total_orders',
key: 'total_orders',
width: 100,
},
{
title: 'Total Returns',
dataIndex: 'total_returns',
key: 'total_returns',
width: 100,
},
{
title: 'Return Amount',
dataIndex: 'return_amount',
key: 'return_amount',
width: 120,
render: (amount: number) => `$${amount.toFixed(2)}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const config = RETURN_STATUS[status as keyof typeof RETURN_STATUS];
return (
<Tag color={config.color} icon={config.icon}>
{config.text}
</Tag>
);
},
},
{
title: 'Last Return',
dataIndex: 'last_return_date',
key: 'last_return_date',
width: 120,
},
{
title: 'Actions',
key: 'actions',
width: 100,
render: (_: any, record: ReturnData) => (
<Space size="middle">
<Button type="link" icon={<ExclamationCircleOutlined />} onClick={() => handleViewDetail(record)}>
Details
</Button>
</Space>
),
},
];
const handleViewDetail = (record: ReturnData) => {
setSelectedRecord(record);
setModalVisible(true);
};
const handleRefresh = () => {
setLoading(true);
// 模拟数据刷新
setTimeout(() => {
setReturnData([...MOCK_RETURN_DATA]);
setLoading(false);
message.success('数据已刷新');
}, 1000);
};
const handleFilter = (values: any) => {
setFilters({
platform: values.platform || '',
status: values.status || '',
dateRange: values.dateRange || null,
minReturnRate: values.minReturnRate || 0,
maxReturnRate: values.maxReturnRate || 100,
});
};
const getStatusCount = (status: string) => {
return returnData.filter(item => item.status === status).length;
};
const getTotalReturns = () => {
return returnData.reduce((sum, item) => sum + item.total_returns, 0);
};
const getAverageReturnRate = () => {
if (returnData.length === 0) return 0;
const totalRate = returnData.reduce((sum, item) => sum + item.return_rate, 0);
return totalRate / returnData.length;
};
return (
<div style={{ padding: '24px' }}>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={16}>
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>退</h1>
<p style={{ color: '#666', margin: '8px 0 0 0' }}>SKU退货率</p>
</Col>
<Col span={8} style={{ textAlign: 'right' }}>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
>
</Button>
<Button icon={<DownloadOutlined />}>
</Button>
</Space>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总退货数"
value={getTotalReturns()}
prefix={<WarningOutlined />}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="平均退货率"
value={((getAverageReturnRate()) * 100).toFixed(1)}
suffix="%"
valueStyle={{ color: getAverageReturnRate() > 0.2 ? '#ff4d4f' : getAverageReturnRate() > 0.1 ? '#fa8c16' : '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="高风险SKU"
value={getStatusCount('HIGH_RISK')}
prefix={<ExclamationCircleOutlined />}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="警告SKU"
value={getStatusCount('WARNING')}
prefix={<WarningOutlined />}
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
</Row>
<Card style={{ marginBottom: 16 }}>
<Tabs defaultActiveKey="1">
<TabPane tab="退货率趋势" key="1">
<div style={{ height: 400 }}>
<ResponsiveContainer width="100%" height="100%">
<Line
data={MOCK_RETURN_TRENDS}
margin={{ top: 20, right: 30, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<RechartsTooltip />
<Legend />
<Line yAxisId="left" type="monotone" dataKey="return_rate" name="退货率" stroke="#ff4d4f" strokeWidth={2} />
<Line yAxisId="right" type="monotone" dataKey="order_count" name="订单数" stroke="#1890ff" />
<Line yAxisId="right" type="monotone" dataKey="return_count" name="退货数" stroke="#fa8c16" />
</Line>
</ResponsiveContainer>
</div>
</TabPane>
<TabPane tab="退货原因分析" key="2">
<div style={{ height: 400 }}>
<ResponsiveContainer width="100%" height="100%">
<Bar
data={RETURN_REASONS.map(reason => ({
name: reason,
value: Math.floor(Math.random() * 50) + 10,
}))}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="value" name="退货数量" fill="#1890ff" />
</Bar>
</ResponsiveContainer>
</div>
</TabPane>
</Tabs>
</Card>
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>SKU退货率列表</h3>
<Button icon={<FilterOutlined />} type="primary" onClick={() => {/* 打开筛选面板 */}}>
</Button>
</div>
<Table
columns={columns}
dataSource={returnData}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
</Card>
<Modal
title="SKU退货详情"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={[
<Button key="close" onClick={() => setModalVisible(false)}>
</Button>,
]}
width={800}
>
{selectedRecord && (
<div>
<Descriptions bordered column={2}>
<Descriptions.Item label="SKU ID">{selectedRecord.sku_id}</Descriptions.Item>
<Descriptions.Item label="SKU Name">{selectedRecord.sku_name}</Descriptions.Item>
<Descriptions.Item label="Product Name">{selectedRecord.product_name}</Descriptions.Item>
<Descriptions.Item label="Return Rate">
<span style={{ color: selectedRecord.return_rate > 0.2 ? 'red' : selectedRecord.return_rate > 0.1 ? 'orange' : 'green' }}>
{((selectedRecord.return_rate) * 100).toFixed(1)}%
</span>
</Descriptions.Item>
<Descriptions.Item label="Total Orders">{selectedRecord.total_orders}</Descriptions.Item>
<Descriptions.Item label="Total Returns">{selectedRecord.total_returns}</Descriptions.Item>
<Descriptions.Item label="Return Amount">${selectedRecord.return_amount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="Avg Processing Time">{selectedRecord.avg_processing_time} days</Descriptions.Item>
<Descriptions.Item label="Status" span={2}>
<Tag color={RETURN_STATUS[selectedRecord.status].color} icon={RETURN_STATUS[selectedRecord.status].icon}>
{RETURN_STATUS[selectedRecord.status].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Last Return Date">{selectedRecord.last_return_date}</Descriptions.Item>
<Descriptions.Item label="Created At">{selectedRecord.created_at}</Descriptions.Item>
</Descriptions>
<Divider orientation="left">退</Divider>
<Space wrap>
{selectedRecord.return_reasons.map((reason, index) => (
<Tag key={index}>{reason}</Tag>
))}
</Space>
<Divider orientation="left">退</Divider>
<div style={{ height: 300 }}>
<ResponsiveContainer width="100%" height="100%">
<Line
data={MOCK_RETURN_TRENDS.slice(-7)}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<RechartsTooltip />
<Line type="monotone" dataKey="return_rate" stroke="#ff4d4f" activeDot={{ r: 8 }} />
</Line>
</ResponsiveContainer>
</div>
</div>
)}
</Modal>
</div>
);
};
export default ReturnMonitor;

View File

@@ -0,0 +1,560 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
InputNumber,
Descriptions,
Divider,
message,
Tag,
Switch,
Popconfirm,
Alert,
Tooltip,
Badge,
Spin,
Popover,
Row,
Col,
Statistic,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
EyeOutlined,
StopOutlined,
PlayCircleOutlined,
WarningOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined,
DownloadOutlined,
UploadOutlined,
PlusOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Option } = Select;
const { TextArea } = Input;
interface SKUData {
id: string;
tenant_id: string;
shop_id: string;
sku_id: string;
sku_name: string;
product_name: string;
platform: string;
price: number;
stock: number;
return_rate: number;
status: 'ACTIVE' | 'INACTIVE' | 'AUTO_REMOVED';
auto_removed_reason?: string;
last_updated: string;
created_at: string;
}
const SKU_STATUS: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
ACTIVE: { color: 'green', text: '在售', icon: <CheckCircleOutlined /> },
INACTIVE: { color: 'default', text: '下架', icon: <StopOutlined /> },
AUTO_REMOVED: { color: 'red', text: '自动下架', icon: <ExclamationCircleOutlined /> },
};
const PLATFORMS = [
'Amazon',
'eBay',
'Shopify',
'Walmart',
'TikTok Shop',
'Temu',
'Alibaba',
'Other',
];
const MOCK_SKU_DATA: SKUData[] = [
{
id: '1',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-001',
sku_name: 'Wireless Bluetooth Headphones',
product_name: 'Premium Wireless Headphones',
platform: 'Amazon',
price: 200.00,
stock: 50,
return_rate: 0.35,
status: 'AUTO_REMOVED',
auto_removed_reason: '退货率超过阈值 (35%)',
last_updated: '2026-03-17',
created_at: '2026-03-01',
},
{
id: '2',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-002',
sku_name: 'Smart Fitness Tracker',
product_name: 'Fitness Tracker Pro',
platform: 'eBay',
price: 150.00,
stock: 30,
return_rate: 0.15,
status: 'ACTIVE',
last_updated: '2026-03-16',
created_at: '2026-03-01',
},
{
id: '3',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-003',
sku_name: 'Portable Power Bank',
product_name: '10000mAh Power Bank',
platform: 'Shopify',
price: 50.00,
stock: 100,
return_rate: 0.08,
status: 'ACTIVE',
last_updated: '2026-03-15',
created_at: '2026-03-01',
},
{
id: '4',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-004',
sku_name: 'Bluetooth Speaker',
product_name: 'Portable Bluetooth Speaker',
platform: 'Walmart',
price: 80.00,
stock: 40,
return_rate: 0.22,
status: 'ACTIVE',
last_updated: '2026-03-17',
created_at: '2026-03-01',
},
{
id: '5',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-005',
sku_name: 'USB-C Cable',
product_name: 'Fast Charging USB-C Cable',
platform: 'Amazon',
price: 10.00,
stock: 200,
return_rate: 0.05,
status: 'ACTIVE',
last_updated: '2026-03-14',
created_at: '2026-03-01',
},
{
id: '6',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-006',
sku_name: 'Wireless Mouse',
product_name: 'Ergonomic Wireless Mouse',
platform: 'eBay',
price: 30.00,
stock: 60,
return_rate: 0.12,
status: 'ACTIVE',
last_updated: '2026-03-16',
created_at: '2026-03-01',
},
{
id: '7',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-007',
sku_name: 'Laptop Case',
product_name: 'Protective Laptop Case',
platform: 'Shopify',
price: 45.00,
stock: 25,
return_rate: 0.18,
status: 'ACTIVE',
last_updated: '2026-03-15',
created_at: '2026-03-01',
},
{
id: '8',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
sku_id: 'SKU-008',
sku_name: 'Smart Watch',
product_name: 'Smart Watch Series 5',
platform: 'Amazon',
price: 250.00,
stock: 20,
return_rate: 0.28,
status: 'AUTO_REMOVED',
auto_removed_reason: '退货率超过阈值 (28%)',
last_updated: '2026-03-17',
created_at: '2026-03-01',
},
];
const SKUManage: React.FC = () => {
const [skuData, setSkuData] = useState<SKUData[]>(MOCK_SKU_DATA);
const [loading, setLoading] = useState(false);
const [selectedSku, setSelectedSku] = useState<SKUData | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [form] = Form.useForm();
const columns: ColumnsType<SKUData> = [
{
title: 'SKU ID',
dataIndex: 'sku_id',
key: 'sku_id',
width: 120,
},
{
title: 'SKU Name',
dataIndex: 'sku_name',
key: 'sku_name',
width: 200,
render: (name: string, record: SKUData) => (
<a onClick={() => handleViewDetail(record)}>{name}</a>
),
},
{
title: 'Product Name',
dataIndex: 'product_name',
key: 'product_name',
width: 200,
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
},
{
title: 'Price',
dataIndex: 'price',
key: 'price',
width: 100,
render: (price: number) => `$${price.toFixed(2)}`,
},
{
title: 'Stock',
dataIndex: 'stock',
key: 'stock',
width: 80,
},
{
title: 'Return Rate',
dataIndex: 'return_rate',
key: 'return_rate',
width: 120,
render: (rate: number) => (
<div>
<span style={{ color: rate > 0.2 ? 'red' : rate > 0.1 ? 'orange' : 'green' }}>
{((rate) * 100).toFixed(1)}%
</span>
</div>
),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status: string, record: SKUData) => {
const config = SKU_STATUS[status as keyof typeof SKU_STATUS];
return (
<Popover content={record.auto_removed_reason || ''}>
<Tag color={config.color} icon={config.icon}>
{config.text}
</Tag>
</Popover>
);
},
},
{
title: 'Last Updated',
dataIndex: 'last_updated',
key: 'last_updated',
width: 120,
},
{
title: 'Actions',
key: 'actions',
width: 150,
render: (_: any, record: SKUData) => (
<Space size="middle">
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
View
</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
Edit
</Button>
{record.status === 'ACTIVE' ? (
<Popconfirm
title="确定要下架这个SKU吗"
onConfirm={() => handleToggleStatus(record.id, 'INACTIVE')}
okText="确定"
cancelText="取消"
>
<Button type="link" icon={<StopOutlined />} style={{ color: 'orange' }}>
</Button>
</Popconfirm>
) : (
<Button
type="link"
icon={<PlayCircleOutlined />}
style={{ color: 'green' }}
onClick={() => handleToggleStatus(record.id, 'ACTIVE')}
>
</Button>
)}
</Space>
),
},
];
const handleViewDetail = (record: SKUData) => {
setSelectedSku(record);
setModalVisible(true);
};
const handleEdit = (record: SKUData) => {
form.setFieldsValue({
sku_id: record.sku_id,
sku_name: record.sku_name,
product_name: record.product_name,
platform: record.platform,
price: record.price,
stock: record.stock,
});
setSelectedSku(record);
setModalVisible(true);
};
const handleToggleStatus = (id: string, newStatus: 'ACTIVE' | 'INACTIVE') => {
setLoading(true);
setTimeout(() => {
setSkuData(skuData.map(item =>
item.id === id ? { ...item, status: newStatus, last_updated: new Date().toISOString().split('T')[0] } : item
));
setLoading(false);
message.success(`${newStatus === 'ACTIVE' ? '上架' : '下架'}成功`);
}, 500);
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setConfirmLoading(true);
setTimeout(() => {
setSkuData(skuData.map(item =>
item.id === selectedSku?.id
? {
...item,
...values,
last_updated: new Date().toISOString().split('T')[0]
}
: item
));
setConfirmLoading(false);
setModalVisible(false);
message.success('SKU信息更新成功');
}, 1000);
} catch (error) {
console.error('Validation failed:', error);
}
};
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setSkuData([...MOCK_SKU_DATA]);
setLoading(false);
message.success('数据已刷新');
}, 1000);
};
const getStatusCount = (status: string) => {
return skuData.filter(item => item.status === status).length;
};
const getHighRiskCount = () => {
return skuData.filter(item => item.return_rate > 0.2).length;
};
return (
<div style={{ padding: '24px' }}>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={16}>
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>SKU管理中心</h1>
<p style={{ color: '#666', margin: '8px 0 0 0' }}>SKU信息退</p>
</Col>
<Col span={8} style={{ textAlign: 'right' }}>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
>
</Button>
<Button icon={<DownloadOutlined />}>
</Button>
<Button icon={<UploadOutlined />} type="primary">
</Button>
</Space>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总SKU数"
value={skuData.length}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="在售SKU"
value={getStatusCount('ACTIVE')}
prefix={<PlayCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="自动下架"
value={getStatusCount('AUTO_REMOVED')}
prefix={<ExclamationCircleOutlined />}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="高风险SKU"
value={getHighRiskCount()}
prefix={<WarningOutlined />}
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
</Row>
<Alert
message="高退货率SKU提醒"
description={`当前有 ${getHighRiskCount()} 个SKU退货率超过20%,请及时处理`}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>SKU列表</h3>
<Button type="primary" icon={<PlusOutlined />}>
SKU
</Button>
</div>
<Table
columns={columns}
dataSource={skuData}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
</Card>
<Modal
title={selectedSku ? "编辑SKU信息" : "添加SKU"}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSubmit}
confirmLoading={confirmLoading}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item
name="sku_id"
label="SKU ID"
rules={[{ required: true, message: '请输入SKU ID' }]}
>
<Input placeholder="请输入SKU ID" />
</Form.Item>
<Form.Item
name="sku_name"
label="SKU Name"
rules={[{ required: true, message: '请输入SKU名称' }]}
>
<Input placeholder="请输入SKU名称" />
</Form.Item>
<Form.Item
name="product_name"
label="Product Name"
rules={[{ required: true, message: '请输入产品名称' }]}
>
<Input placeholder="请输入产品名称" />
</Form.Item>
<Form.Item
name="platform"
label="Platform"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select placeholder="请选择平台">
{PLATFORMS.map(platform => (
<Option key={platform} value={platform}>{platform}</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="price"
label="Price"
rules={[{ required: true, message: '请输入价格' }]}
>
<InputNumber
style={{ width: '100%' }}
placeholder="请输入价格"
min={0}
step={0.01}
formatter={(value) => `$ ${value}`}
parser={(value) => value?.replace(/^\$\s*/, '')}
/>
</Form.Item>
<Form.Item
name="stock"
label="Stock"
rules={[{ required: true, message: '请输入库存' }]}
>
<InputNumber
style={{ width: '100%' }}
placeholder="请输入库存"
min={0}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default SKUManage;

View File

@@ -0,0 +1,2 @@
export { default as ReturnMonitor } from './ReturnMonitor';
export { default as SKUManage } from './SKUManage';

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { Card, Layout, Menu, Typography, Row, Col, Statistic } from 'antd';
import {
DashboardOutlined,
ShoppingOutlined,
UserOutlined,
TruckOutlined,
AuditOutlined,
AlertOutlined,
FileTextOutlined,
DollarOutlined,
BarChartOutlined
} from '@ant-design/icons';
import { Link } from 'umi';
const { Header, Content, Sider } = Layout;
const { Title, Text } = Typography;
const Dashboard: React.FC = () => {
const menuItems = [
{
key: 'dashboard',
icon: <DashboardOutlined />,
label: <Link to="/"></Link>
},
{
key: 'products',
icon: <ShoppingOutlined />,
label: <Link to="/Product"></Link>
},
{
key: 'orders',
icon: <FileTextOutlined />,
label: <Link to="/Orders"></Link>
},
{
key: 'merchants',
icon: <UserOutlined />,
label: <Link to="/Merchant"></Link>
},
{
key: 'logistics',
icon: <TruckOutlined />,
label: <Link to="/Logistics"></Link>
},
{
key: 'aftersales',
icon: <AlertOutlined />,
label: <Link to="/AfterSales"></Link>
},
{
key: 'compliance',
icon: <AuditOutlined />,
label: <Link to="/Compliance"></Link>
},
{
key: 'blacklist',
icon: <AlertOutlined />,
label: <Link to="/Blacklist"></Link>
},
{
key: 'b2b',
icon: <DollarOutlined />,
label: <Link to="/B2B">B2B贸易</Link>
},
{
key: 'ads',
icon: <BarChartOutlined />,
label: <Link to="/Ad">广</Link>
}
];
return (
<Layout style={{ minHeight: '100vh' }}>
<Header style={{ display: 'flex', alignItems: 'center', background: '#1890ff' }}>
<Title level={3} style={{ color: 'white', margin: 0 }}>Crawlful Hub </Title>
</Header>
<Layout>
<Sider width={200} style={{ background: '#f0f2f5' }}>
<Menu
mode="inline"
style={{ height: '100%', borderRight: 0 }}
defaultSelectedKeys={['dashboard']}
items={menuItems}
/>
</Sider>
<Layout style={{ padding: '0 24px 24px' }}>
<Content style={{ padding: 24, margin: 0, minHeight: 280, background: '#fff' }}>
<Title level={4}></Title>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card>
<Statistic title="商品数量" value={1280} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="订单数量" value={960} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="商户数量" value={120} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="总销售额" value={1280000} prefix="¥" />
</Card>
</Col>
</Row>
<div style={{ marginTop: 24 }}>
<Card title="系统功能">
<Text>
使 Crawlful Hub
</Text>
</Card>
</div>
</Content>
</Layout>
</Layout>
</Layout>
);
};
export default Dashboard;