refactor: 重构页面组件移除冗余Layout组件 feat: 实现WebSocket和事件总线系统 feat: 添加队列和调度系统 docs: 更新架构文档和服务映射 style: 清理重复接口定义使用数据源 chore: 更新依赖项配置 feat: 添加运行时系统和领域引导 ci: 配置ESLint边界检查规则 build: 添加Redis和WebSocket依赖 test: 添加MSW浏览器环境入口 perf: 优化数据获取逻辑使用统一数据源 fix: 修复类型定义和状态管理问题
894 lines
26 KiB
TypeScript
894 lines
26 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Card,
|
||
Table,
|
||
Button,
|
||
Space,
|
||
Tag,
|
||
Input,
|
||
Select,
|
||
Row,
|
||
Col,
|
||
Modal,
|
||
message,
|
||
Tooltip,
|
||
Badge,
|
||
Dropdown,
|
||
Menu,
|
||
Image,
|
||
Typography,
|
||
DatePicker,
|
||
Form,
|
||
Drawer,
|
||
Alert,
|
||
Spin,
|
||
} from 'antd';
|
||
import {
|
||
PlusOutlined,
|
||
EditOutlined,
|
||
DeleteOutlined,
|
||
SyncOutlined,
|
||
UploadOutlined,
|
||
FilterOutlined,
|
||
SortAscendingOutlined,
|
||
SortDescendingOutlined,
|
||
MoreOutlined,
|
||
EyeOutlined,
|
||
CopyOutlined,
|
||
ArrowUpOutlined,
|
||
ArrowDownOutlined,
|
||
DollarOutlined,
|
||
ShoppingOutlined,
|
||
CheckCircleOutlined,
|
||
CloseCircleOutlined,
|
||
ExclamationCircleOutlined,
|
||
} from '@ant-design/icons';
|
||
import { Link, history } from 'umi';
|
||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||
import type { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||
import moment from 'moment';
|
||
|
||
const { Title, Text } = Typography;
|
||
const { Option } = Select;
|
||
const { RangePicker } = DatePicker;
|
||
const { Search } = Input;
|
||
|
||
interface Product {
|
||
id: string;
|
||
sku: string;
|
||
name: string;
|
||
image: string;
|
||
category: string;
|
||
price: number;
|
||
costPrice: number;
|
||
profit: number;
|
||
roi: number;
|
||
stock: number;
|
||
status: 'DRAFT' | 'PRICED' | 'LISTED' | 'SYNCING' | 'LIVE' | 'SYNC_FAILED' | 'OFFLINE';
|
||
platformStatus: Record<string, string>;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
interface FilterState {
|
||
keyword: string;
|
||
status: string[];
|
||
platform: string[];
|
||
category: string[];
|
||
roiRange: [number, number] | null;
|
||
dateRange: [moment.Moment, moment.Moment] | null;
|
||
}
|
||
|
||
interface SortState {
|
||
field: string;
|
||
order: 'ascend' | 'descend' | null;
|
||
}
|
||
|
||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||
DRAFT: { color: 'default', text: '草稿', icon: <EditOutlined /> },
|
||
PRICED: { color: 'processing', text: '已定价', icon: <DollarOutlined /> },
|
||
LISTED: { color: 'warning', text: '已上架', icon: <ShoppingOutlined /> },
|
||
SYNCING: { color: 'processing', text: '同步中', icon: <SyncOutlined spin /> },
|
||
LIVE: { color: 'success', text: '已在线', icon: <CheckCircleOutlined /> },
|
||
SYNC_FAILED: { color: 'error', text: '同步失败', icon: <CloseCircleOutlined /> },
|
||
OFFLINE: { color: 'default', text: '已下架', icon: <ExclamationCircleOutlined /> },
|
||
};
|
||
|
||
const PLATFORMS = ['Amazon', 'eBay', 'Shopee', 'TikTok', 'Shopify'];
|
||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||
|
||
const MOCK_PRODUCTS: Product[] = [
|
||
{
|
||
id: '1',
|
||
sku: 'TP-TEMP-001',
|
||
name: '工业温度传感器 Pro',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工业自动化',
|
||
price: 89.99,
|
||
costPrice: 45.00,
|
||
profit: 44.99,
|
||
roi: 99.98,
|
||
stock: 256,
|
||
status: 'LIVE',
|
||
platformStatus: { Amazon: 'LIVE', eBay: 'LIVE', Shopee: 'PENDING' },
|
||
createdAt: '2025-12-15',
|
||
updatedAt: '2026-03-18',
|
||
},
|
||
{
|
||
id: '2',
|
||
sku: 'TP-PRES-002',
|
||
name: '压力传感器 Digital',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工业自动化',
|
||
price: 129.99,
|
||
costPrice: 65.00,
|
||
profit: 64.99,
|
||
roi: 99.98,
|
||
stock: 128,
|
||
status: 'DRAFT',
|
||
platformStatus: {},
|
||
createdAt: '2026-03-10',
|
||
updatedAt: '2026-03-10',
|
||
},
|
||
{
|
||
id: '3',
|
||
sku: 'TP-FLOW-003',
|
||
name: '流量计 Ultra',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '仪器仪表',
|
||
price: 299.99,
|
||
costPrice: 150.00,
|
||
profit: 149.99,
|
||
roi: 99.99,
|
||
stock: 64,
|
||
status: 'PRICED',
|
||
platformStatus: {},
|
||
createdAt: '2026-03-15',
|
||
updatedAt: '2026-03-16',
|
||
},
|
||
{
|
||
id: '4',
|
||
sku: 'TP-MOTR-004',
|
||
name: '步进电机 57型',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工业自动化',
|
||
price: 59.99,
|
||
costPrice: 30.00,
|
||
profit: 29.99,
|
||
roi: 99.97,
|
||
stock: 512,
|
||
status: 'LISTED',
|
||
platformStatus: { Amazon: 'LISTED' },
|
||
createdAt: '2026-03-01',
|
||
updatedAt: '2026-03-17',
|
||
},
|
||
{
|
||
id: '5',
|
||
sku: 'TP-CTRL-005',
|
||
name: 'PLC控制器 Mini',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工业自动化',
|
||
price: 199.99,
|
||
costPrice: 100.00,
|
||
profit: 99.99,
|
||
roi: 99.99,
|
||
stock: 32,
|
||
status: 'SYNCING',
|
||
platformStatus: { Amazon: 'SYNCING', eBay: 'SYNCING' },
|
||
createdAt: '2026-03-18',
|
||
updatedAt: '2026-03-18',
|
||
},
|
||
{
|
||
id: '6',
|
||
sku: 'TP-SENS-006',
|
||
name: '光电传感器',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工业自动化',
|
||
price: 39.99,
|
||
costPrice: 20.00,
|
||
profit: 19.99,
|
||
roi: 99.95,
|
||
stock: 0,
|
||
status: 'SYNC_FAILED',
|
||
platformStatus: { Amazon: 'FAILED' },
|
||
createdAt: '2026-03-05',
|
||
updatedAt: '2026-03-18',
|
||
},
|
||
{
|
||
id: '7',
|
||
sku: 'TP-VALV-007',
|
||
name: '电磁阀 24V',
|
||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||
category: '工具设备',
|
||
price: 79.99,
|
||
costPrice: 40.00,
|
||
profit: 39.99,
|
||
roi: 99.98,
|
||
stock: 200,
|
||
status: 'OFFLINE',
|
||
platformStatus: { Amazon: 'OFFLINE', eBay: 'OFFLINE' },
|
||
createdAt: '2025-11-20',
|
||
updatedAt: '2026-02-28',
|
||
},
|
||
];
|
||
|
||
export const ProductList: React.FC = () => {
|
||
const [products, setProducts] = useState<Product[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedRows, setSelectedRows] = useState<Product[]>([]);
|
||
const [filterVisible, setFilterVisible] = useState(false);
|
||
const [sortDrawerVisible, setSortDrawerVisible] = useState(false);
|
||
const [pricingModalVisible, setPricingModalVisible] = useState(false);
|
||
const [currentProduct, setCurrentProduct] = useState<Product | null>(null);
|
||
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
|
||
|
||
const [filters, setFilters] = useState<FilterState>({
|
||
keyword: '',
|
||
status: [],
|
||
platform: [],
|
||
category: [],
|
||
roiRange: null,
|
||
dateRange: null,
|
||
});
|
||
|
||
const [sort, setSort] = useState<SortState>({
|
||
field: 'updatedAt',
|
||
order: 'descend',
|
||
});
|
||
|
||
useEffect(() => {
|
||
fetchProducts();
|
||
}, []);
|
||
|
||
const fetchProducts = useCallback(async () => {
|
||
setLoading(true);
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
setProducts(MOCK_PRODUCTS);
|
||
setLoading(false);
|
||
}, []);
|
||
|
||
const handleFilterChange = (key: keyof FilterState, value: any) => {
|
||
setFilters(prev => ({ ...prev, [key]: value }));
|
||
};
|
||
|
||
const handleSortChange = (field: string, order: 'ascend' | 'descend') => {
|
||
setSort({ field, order });
|
||
setSortDrawerVisible(false);
|
||
message.success(`已按 ${field} ${order === 'ascend' ? '升序' : '降序'} 排序`);
|
||
};
|
||
|
||
const handleTableChange = (
|
||
pagination: TablePaginationConfig,
|
||
filters: Record<string, FilterValue | null>,
|
||
sorter: SorterResult<Product> | SorterResult<Product>[],
|
||
extra: TableCurrentDataSource<Product>
|
||
) => {
|
||
if (!Array.isArray(sorter) && sorter.field) {
|
||
setSort({
|
||
field: sorter.field as string,
|
||
order: sorter.order || null,
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleSearch = (value: string) => {
|
||
handleFilterChange('keyword', value);
|
||
};
|
||
|
||
const handleResetFilters = () => {
|
||
setFilters({
|
||
keyword: '',
|
||
status: [],
|
||
platform: [],
|
||
category: [],
|
||
roiRange: null,
|
||
dateRange: null,
|
||
});
|
||
message.success('筛选条件已重置');
|
||
};
|
||
|
||
const handleAddProduct = () => {
|
||
history.push('/Product/ProductPublishForm');
|
||
};
|
||
|
||
const handleEditProduct = (record: Product) => {
|
||
history.push(`/Product/ProductDetail?id=${record.id}`);
|
||
};
|
||
|
||
const handleViewProduct = (record: Product) => {
|
||
history.push(`/Product/ProductDetail?id=${record.id}`);
|
||
};
|
||
|
||
const handleDeleteProduct = (record: Product) => {
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: `确定要删除商品 "${record.name}" 吗?此操作不可恢复。`,
|
||
okText: '删除',
|
||
okType: 'danger',
|
||
onOk: () => {
|
||
setProducts(products.filter(p => p.id !== record.id));
|
||
message.success('商品删除成功');
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleDuplicateProduct = (record: Product) => {
|
||
const newProduct: Product = {
|
||
...record,
|
||
id: `${Date.now()}`,
|
||
sku: `${record.sku}-COPY`,
|
||
name: `${record.name} (复制)`,
|
||
status: 'DRAFT',
|
||
platformStatus: {},
|
||
createdAt: moment().format('YYYY-MM-DD'),
|
||
updatedAt: moment().format('YYYY-MM-DD'),
|
||
};
|
||
setProducts([newProduct, ...products]);
|
||
message.success('商品复制成功');
|
||
};
|
||
|
||
const handlePricing = (record: Product) => {
|
||
setCurrentProduct(record);
|
||
setPricingModalVisible(true);
|
||
};
|
||
|
||
const handlePublish = (record: Product) => {
|
||
Modal.confirm({
|
||
title: '确认上架',
|
||
content: `确定要上架商品 "${record.name}" 吗?`,
|
||
onOk: () => {
|
||
updateProductStatus(record.id, 'LISTED');
|
||
message.success('商品上架成功');
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleSync = async (record: Product) => {
|
||
setSyncLoading(prev => ({ ...prev, [record.id]: true }));
|
||
|
||
updateProductStatus(record.id, 'SYNCING');
|
||
message.loading({ content: '正在同步到平台...', key: record.id });
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
const success = Math.random() > 0.3;
|
||
if (success) {
|
||
updateProductStatus(record.id, 'LIVE');
|
||
message.success({ content: '同步成功', key: record.id });
|
||
} else {
|
||
updateProductStatus(record.id, 'SYNC_FAILED');
|
||
message.error({ content: '同步失败,请重试', key: record.id });
|
||
}
|
||
|
||
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
|
||
};
|
||
|
||
const handleOffline = (record: Product) => {
|
||
Modal.confirm({
|
||
title: '确认下架',
|
||
content: `确定要下架商品 "${record.name}" 吗?`,
|
||
onOk: () => {
|
||
updateProductStatus(record.id, 'OFFLINE');
|
||
message.success('商品已下架');
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleRetrySync = (record: Product) => {
|
||
handleSync(record);
|
||
};
|
||
|
||
const updateProductStatus = (productId: string, status: Product['status']) => {
|
||
setProducts(products.map(p =>
|
||
p.id === productId ? { ...p, status, updatedAt: moment().format('YYYY-MM-DD') } : p
|
||
));
|
||
};
|
||
|
||
const handleBatchDelete = () => {
|
||
if (selectedRows.length === 0) {
|
||
message.warning('请先选择要删除的商品');
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '确认批量删除',
|
||
content: `确定要删除选中的 ${selectedRows.length} 个商品吗?`,
|
||
okText: '删除',
|
||
okType: 'danger',
|
||
onOk: () => {
|
||
const ids = selectedRows.map(r => r.id);
|
||
setProducts(products.filter(p => !ids.includes(p.id)));
|
||
setSelectedRows([]);
|
||
message.success(`成功删除 ${ids.length} 个商品`);
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleBatchPricing = () => {
|
||
if (selectedRows.length === 0) {
|
||
message.warning('请先选择要定价的商品');
|
||
return;
|
||
}
|
||
message.info(`批量定价功能:已选择 ${selectedRows.length} 个商品`);
|
||
};
|
||
|
||
const handleBatchPublish = () => {
|
||
if (selectedRows.length === 0) {
|
||
message.warning('请先选择要上架的商品');
|
||
return;
|
||
}
|
||
const pricedProducts = selectedRows.filter(p => p.status === 'PRICED');
|
||
if (pricedProducts.length === 0) {
|
||
message.warning('选中的商品中没有已定价的商品');
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '确认批量上架',
|
||
content: `确定要上架选中的 ${pricedProducts.length} 个商品吗?`,
|
||
onOk: () => {
|
||
pricedProducts.forEach(p => updateProductStatus(p.id, 'LISTED'));
|
||
setSelectedRows([]);
|
||
message.success(`成功上架 ${pricedProducts.length} 个商品`);
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleBatchSync = () => {
|
||
if (selectedRows.length === 0) {
|
||
message.warning('请先选择要同步的商品');
|
||
return;
|
||
}
|
||
const listableProducts = selectedRows.filter(p => p.status === 'LISTED' || p.status === 'LIVE');
|
||
if (listableProducts.length === 0) {
|
||
message.warning('选中的商品中没有可同步的商品');
|
||
return;
|
||
}
|
||
message.loading('正在批量同步...');
|
||
setTimeout(() => {
|
||
listableProducts.forEach(p => handleSync(p));
|
||
setSelectedRows([]);
|
||
}, 500);
|
||
};
|
||
|
||
const filteredProducts = products.filter(product => {
|
||
if (filters.keyword && !product.name.toLowerCase().includes(filters.keyword.toLowerCase()) &&
|
||
!product.sku.toLowerCase().includes(filters.keyword.toLowerCase())) {
|
||
return false;
|
||
}
|
||
if (filters.status.length > 0 && !filters.status.includes(product.status)) {
|
||
return false;
|
||
}
|
||
if (filters.category.length > 0 && !filters.category.includes(product.category)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
const sortedProducts = [...filteredProducts].sort((a, b) => {
|
||
const field = sort.field as keyof Product;
|
||
const aValue = a[field];
|
||
const bValue = b[field];
|
||
|
||
if (sort.order === 'ascend') {
|
||
return aValue > bValue ? 1 : -1;
|
||
} else {
|
||
return aValue < bValue ? 1 : -1;
|
||
}
|
||
});
|
||
|
||
const columns: ColumnsType<Product> = [
|
||
{
|
||
title: '商品信息',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
render: (text, record) => (
|
||
<Space>
|
||
<Image src={record.image} width={60} height={60} style={{ objectFit: 'cover' }} />
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||
<div style={{ fontSize: 12, color: '#999' }}>SKU: {record.sku}</div>
|
||
<div style={{ fontSize: 12, color: '#666' }}>{record.category}</div>
|
||
</div>
|
||
</Space>
|
||
),
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '售价',
|
||
dataIndex: 'price',
|
||
key: 'price',
|
||
render: (value) => <Text strong>${value.toFixed(2)}</Text>,
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '成本',
|
||
dataIndex: 'costPrice',
|
||
key: 'costPrice',
|
||
render: (value) => <Text type="secondary">${value.toFixed(2)}</Text>,
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '利润',
|
||
dataIndex: 'profit',
|
||
key: 'profit',
|
||
render: (value) => <Text type="success">${value.toFixed(2)}</Text>,
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: 'ROI',
|
||
dataIndex: 'roi',
|
||
key: 'roi',
|
||
render: (value) => <Tag color="green">{value.toFixed(2)}%</Tag>,
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '库存',
|
||
dataIndex: 'stock',
|
||
key: 'stock',
|
||
render: (value) => (
|
||
<Badge
|
||
count={value}
|
||
style={{ backgroundColor: value > 50 ? '#52c41a' : value > 0 ? '#faad14' : '#ff4d4f' }}
|
||
/>
|
||
),
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
render: (status) => {
|
||
const config = STATUS_CONFIG[status];
|
||
return (
|
||
<Tag color={config.color} icon={config.icon}>
|
||
{config.text}
|
||
</Tag>
|
||
);
|
||
},
|
||
filters: Object.entries(STATUS_CONFIG).map(([key, config]) => ({
|
||
text: config.text,
|
||
value: key,
|
||
})),
|
||
},
|
||
{
|
||
title: '平台状态',
|
||
key: 'platformStatus',
|
||
render: (_, record) => (
|
||
<Space size="small">
|
||
{Object.entries(record.platformStatus).map(([platform, status]) => (
|
||
<Tooltip key={platform} title={`${platform}: ${status}`}>
|
||
<Tag
|
||
size="small"
|
||
color={status === 'LIVE' ? 'success' : status === 'FAILED' ? 'error' : 'processing'}
|
||
>
|
||
{platform.charAt(0)}
|
||
</Tag>
|
||
</Tooltip>
|
||
))}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '更新时间',
|
||
dataIndex: 'updatedAt',
|
||
key: 'updatedAt',
|
||
sorter: true,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
fixed: 'right',
|
||
width: 200,
|
||
render: (_, record) => {
|
||
const menu = (
|
||
<Menu>
|
||
<Menu.Item key="view" icon={<EyeOutlined />} onClick={() => handleViewProduct(record)}>
|
||
查看详情
|
||
</Menu.Item>
|
||
<Menu.Item key="edit" icon={<EditOutlined />} onClick={() => handleEditProduct(record)}>
|
||
编辑
|
||
</Menu.Item>
|
||
<Menu.Item key="duplicate" icon={<CopyOutlined />} onClick={() => handleDuplicateProduct(record)}>
|
||
复制
|
||
</Menu.Item>
|
||
<Menu.Item key="pricing" icon={<DollarOutlined />} onClick={() => handlePricing(record)}>
|
||
定价
|
||
</Menu.Item>
|
||
{record.status === 'PRICED' && (
|
||
<Menu.Item key="publish" icon={<ShoppingOutlined />} onClick={() => handlePublish(record)}>
|
||
上架
|
||
</Menu.Item>
|
||
)}
|
||
{(record.status === 'LISTED' || record.status === 'LIVE') && (
|
||
<Menu.Item
|
||
key="sync"
|
||
icon={<SyncOutlined spin={syncLoading[record.id]} />}
|
||
onClick={() => handleSync(record)}
|
||
disabled={syncLoading[record.id]}
|
||
>
|
||
同步到平台
|
||
</Menu.Item>
|
||
)}
|
||
{record.status === 'SYNC_FAILED' && (
|
||
<Menu.Item key="retry" icon={<SyncOutlined />} onClick={() => handleRetrySync(record)}>
|
||
重试同步
|
||
</Menu.Item>
|
||
)}
|
||
{(record.status === 'LISTED' || record.status === 'LIVE') && (
|
||
<Menu.Item key="offline" icon={<CloseCircleOutlined />} onClick={() => handleOffline(record)}>
|
||
下架
|
||
</Menu.Item>
|
||
)}
|
||
<Menu.Divider />
|
||
<Menu.Item key="delete" icon={<DeleteOutlined />} danger onClick={() => handleDeleteProduct(record)}>
|
||
删除
|
||
</Menu.Item>
|
||
</Menu>
|
||
);
|
||
|
||
return (
|
||
<Space>
|
||
<Button type="link" size="small" onClick={() => handleViewProduct(record)}>
|
||
查看
|
||
</Button>
|
||
<Button type="link" size="small" onClick={() => handleEditProduct(record)}>
|
||
编辑
|
||
</Button>
|
||
<Dropdown overlay={menu} placement="bottomRight">
|
||
<Button type="link" size="small" icon={<MoreOutlined />}>
|
||
更多
|
||
</Button>
|
||
</Dropdown>
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
];
|
||
|
||
const rowSelection = {
|
||
selectedRowKeys: selectedRows.map(r => r.id),
|
||
onChange: (selectedRowKeys: React.Key[], selectedRows: Product[]) => {
|
||
setSelectedRows(selectedRows);
|
||
},
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
<Alert
|
||
message="商品管理流程说明"
|
||
description={
|
||
<div>
|
||
<p>1. 新建商品:先保存到本地系统(草稿状态)</p>
|
||
<p>2. 定价上架:完成定价后上架到本地(已上架状态)</p>
|
||
<p>3. 平台同步:手动或自动同步到各销售平台(同步中 → 已在线)</p>
|
||
<p>所有商品数据以本地系统为准,平台数据通过同步机制保持一致</p>
|
||
</div>
|
||
}
|
||
type="info"
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||
<Col span={24}>
|
||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||
<Space>
|
||
<Title level={4} style={{ margin: 0 }}>商品列表</Title>
|
||
<Badge count={sortedProducts.length} showZero style={{ backgroundColor: '#1890ff' }} />
|
||
</Space>
|
||
<Space>
|
||
<Button icon={<FilterOutlined />} onClick={() => setFilterVisible(true)}>
|
||
筛选
|
||
</Button>
|
||
<Button icon={<SortAscendingOutlined />} onClick={() => setSortDrawerVisible(true)}>
|
||
排序
|
||
</Button>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddProduct}>
|
||
新增商品
|
||
</Button>
|
||
</Space>
|
||
</Space>
|
||
</Col>
|
||
<Col span={24}>
|
||
<Search
|
||
placeholder="搜索商品名称或SKU"
|
||
allowClear
|
||
enterButton
|
||
onSearch={handleSearch}
|
||
style={{ maxWidth: 400 }}
|
||
/>
|
||
</Col>
|
||
</Row>
|
||
|
||
{selectedRows.length > 0 && (
|
||
<Alert
|
||
message={`已选择 ${selectedRows.length} 项`}
|
||
type="info"
|
||
showIcon
|
||
action={
|
||
<Space>
|
||
<Button size="small" onClick={handleBatchPricing}>
|
||
批量定价
|
||
</Button>
|
||
<Button size="small" onClick={handleBatchPublish}>
|
||
批量上架
|
||
</Button>
|
||
<Button size="small" onClick={handleBatchSync}>
|
||
批量同步
|
||
</Button>
|
||
<Button size="small" danger onClick={handleBatchDelete}>
|
||
批量删除
|
||
</Button>
|
||
</Space>
|
||
}
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
)}
|
||
|
||
<Table
|
||
rowSelection={rowSelection}
|
||
columns={columns}
|
||
dataSource={sortedProducts}
|
||
loading={loading}
|
||
rowKey="id"
|
||
onChange={handleTableChange}
|
||
scroll={{ x: 1200 }}
|
||
pagination={{
|
||
total: sortedProducts.length,
|
||
pageSize: 10,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
/>
|
||
|
||
<Drawer
|
||
title="筛选条件"
|
||
placement="right"
|
||
onClose={() => setFilterVisible(false)}
|
||
visible={filterVisible}
|
||
width={400}
|
||
>
|
||
<Form layout="vertical">
|
||
<Form.Item label="商品状态">
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择状态"
|
||
value={filters.status}
|
||
onChange={(value) => handleFilterChange('status', value)}
|
||
style={{ width: '100%' }}
|
||
>
|
||
{Object.entries(STATUS_CONFIG).map(([key, config]) => (
|
||
<Option key={key} value={key}>{config.text}</Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item label="商品类目">
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择类目"
|
||
value={filters.category}
|
||
onChange={(value) => handleFilterChange('category', value)}
|
||
style={{ width: '100%' }}
|
||
>
|
||
{CATEGORIES.map(cat => (
|
||
<Option key={cat} value={cat}>{cat}</Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item label="平台">
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择平台"
|
||
value={filters.platform}
|
||
onChange={(value) => handleFilterChange('platform', value)}
|
||
style={{ width: '100%' }}
|
||
>
|
||
{PLATFORMS.map(platform => (
|
||
<Option key={platform} value={platform}>{platform}</Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item label="更新时间">
|
||
<RangePicker
|
||
value={filters.dateRange}
|
||
onChange={(dates) => handleFilterChange('dateRange', dates)}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: 16, borderTop: '1px solid #f0f0f0', background: '#fff' }}>
|
||
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
<Button onClick={handleResetFilters}>重置</Button>
|
||
<Button type="primary" onClick={() => setFilterVisible(false)}>确定</Button>
|
||
</Space>
|
||
</div>
|
||
</Drawer>
|
||
|
||
<Drawer
|
||
title="排序设置"
|
||
placement="right"
|
||
onClose={() => setSortDrawerVisible(false)}
|
||
visible={sortDrawerVisible}
|
||
width={300}
|
||
>
|
||
<Space direction="vertical" style={{ width: '100%' }}>
|
||
{[
|
||
{ key: 'name', label: '商品名称' },
|
||
{ key: 'price', label: '售价' },
|
||
{ key: 'profit', label: '利润' },
|
||
{ key: 'roi', label: 'ROI' },
|
||
{ key: 'stock', label: '库存' },
|
||
{ key: 'updatedAt', label: '更新时间' },
|
||
].map(item => (
|
||
<Card key={item.key} size="small" hoverable>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span>{item.label}</span>
|
||
<Space>
|
||
<Button
|
||
size="small"
|
||
icon={<ArrowUpOutlined />}
|
||
type={sort.field === item.key && sort.order === 'ascend' ? 'primary' : 'default'}
|
||
onClick={() => handleSortChange(item.key, 'ascend')}
|
||
>
|
||
升序
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<ArrowDownOutlined />}
|
||
type={sort.field === item.key && sort.order === 'descend' ? 'primary' : 'default'}
|
||
onClick={() => handleSortChange(item.key, 'descend')}
|
||
>
|
||
降序
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</Space>
|
||
</Drawer>
|
||
|
||
<Modal
|
||
title="商品定价"
|
||
visible={pricingModalVisible}
|
||
onCancel={() => setPricingModalVisible(false)}
|
||
footer={null}
|
||
width={600}
|
||
>
|
||
{currentProduct && (
|
||
<Form layout="vertical">
|
||
<Form.Item label="商品">
|
||
<Text strong>{currentProduct.name}</Text>
|
||
</Form.Item>
|
||
<Form.Item label="当前售价">
|
||
<Text>${currentProduct.price.toFixed(2)}</Text>
|
||
</Form.Item>
|
||
<Form.Item label="新售价">
|
||
<InputNumber
|
||
min={0}
|
||
precision={2}
|
||
style={{ width: '100%' }}
|
||
placeholder="输入新售价"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="成本">
|
||
<InputNumber
|
||
min={0}
|
||
precision={2}
|
||
style={{ width: '100%' }}
|
||
defaultValue={currentProduct.costPrice}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Space>
|
||
<Button type="primary">保存定价</Button>
|
||
<Button onClick={() => setPricingModalVisible(false)}>取消</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
)}
|
||
</Modal>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default ProductList;
|