feat: 实现前端组件库和API服务基础架构
refactor: 移除废弃的AGI策略演进服务 fix: 修正磁盘I/O指标字段命名 chore: 更新项目依赖版本 test: 添加前后端集成测试用例 docs: 更新AI模块接口文档 style: 统一审计日志字段命名规范 perf: 优化Redis订阅连接错误处理 build: 配置多项目工作区结构 ci: 添加Vite开发服务器CORS支持
This commit is contained in:
2
dashboard/src/pages/Blacklist/index.ts
Normal file
2
dashboard/src/pages/Blacklist/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as BlacklistManage } from './BlacklistManage';
|
||||
export { default as RiskMonitor } from './RiskMonitor';
|
||||
541
dashboard/src/pages/Return/ReturnMonitor.tsx
Normal file
541
dashboard/src/pages/Return/ReturnMonitor.tsx
Normal 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;
|
||||
560
dashboard/src/pages/Return/SKUManage.tsx
Normal file
560
dashboard/src/pages/Return/SKUManage.tsx
Normal 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;
|
||||
2
dashboard/src/pages/Return/index.ts
Normal file
2
dashboard/src/pages/Return/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ReturnMonitor } from './ReturnMonitor';
|
||||
export { default as SKUManage } from './SKUManage';
|
||||
126
dashboard/src/pages/index.tsx
Normal file
126
dashboard/src/pages/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user