Files
makemd/dashboard/src/pages/Product/ProductList.tsx
wurenzhi e47beffaf9 feat: 重构前端代码结构并添加Java后端支持
- 重构前端导入和组件结构,优化代码组织
- 添加Java后端基础框架和API实现
- 修复类型定义和接口兼容性问题
- 新增测试页面和工具函数
- 优化国际化支持和错误处理
- 更新依赖配置和构建脚本

新增Java后端模块:
- 实现基础认证、订单、支付等服务
- 添加Swagger API文档支持
- 配置数据库连接和缓存
- 实现国际化消息处理
- 添加安全过滤器和限流控制
2026-03-30 16:51:18 +08:00

1259 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
useState,
useEffect,
useCallback,
useMemo,
useNavigate,
FC,
Card,
Table,
Button,
Space,
Tag,
Input,
InputNumber,
Select,
Row,
Col,
Modal,
message,
Tooltip,
Badge,
Dropdown,
Menu,
Image,
Typography,
DatePicker,
Form,
Drawer,
Alert,
Spin,
Tabs,
Statistic,
PlusOutlined,
EditOutlined,
DeleteOutlined,
SyncOutlined,
UploadOutlined,
FilterOutlined,
SortAscendingOutlined,
SortDescendingOutlined,
MoreOutlined,
EyeOutlined,
CopyOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DollarOutlined,
ShoppingOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
AmazonOutlined,
GlobalOutlined,
ShopOutlined,
AppstoreOutlined,
Title,
Text,
Option,
RangePicker,
Search,
ColumnsType,
TablePaginationConfig,
FilterValue,
SorterResult,
TableCurrentDataSource,
moment,
productDataSource,
Product,
Shop,
ProductFilter,
ProductSort,
PLATFORMS,
} from '@/imports';
import { LoadingState, EmptyState } from '../../components/ui';
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
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 PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }> = {
// TikTok系列
TikTok: { icon: <GlobalOutlined />, color: '#000000' },
TikTokFull: { icon: <GlobalOutlined />, color: '#000000' },
// Shopee系列
Shopee: { icon: <ShopOutlined />, color: '#ee4d2d' },
ShopeeFull: { icon: <ShopOutlined />, color: '#ee4d2d' },
ShopeeLight: { icon: <ShopOutlined />, color: '#ee4d2d' },
// Lazada系列
Lazada: { icon: <ShopOutlined />, color: '#0F156D' },
LazadaFull: { icon: <ShopOutlined />, color: '#0F156D' },
// Temu
TemuFull: { icon: <ShopOutlined />, color: '#96bf48' },
// SHEIN系列
Shein: { icon: <ShopOutlined />, color: '#FF6B6B' },
SheinHalf: { icon: <ShopOutlined />, color: '#FF6B6B' },
// 其他平台
Ozon: { icon: <ShopOutlined />, color: '#FFD700' },
Yandex: { icon: <ShopOutlined />, color: '#FF0000' },
AliExpress: { icon: <ShopOutlined />, color: '#FF6A00' },
AliExpressHalf: { icon: <ShopOutlined />, color: '#FF6A00' },
AliExpressPop: { icon: <ShopOutlined />, color: '#FF6A00' },
Coupang: { icon: <ShopOutlined />, color: '#FF3B30' },
Walmart: { icon: <ShopOutlined />, color: '#007DC6' },
Wildberries: { icon: <ShopOutlined />, color: '#9370DB' },
Allegro: { icon: <ShopOutlined />, color: '#00A870' },
MercadoLibre: { icon: <ShopOutlined />, color: '#6C3483' },
Jumia: { icon: <ShopOutlined />, color: '#FF6B00' },
Joom: { icon: <ShopOutlined />, color: '#8A2BE2' },
Amazon: { icon: <AmazonOutlined />, color: '#ff9900' },
Wish: { icon: <ShopOutlined />, color: '#4A90E2' },
Emag: { icon: <ShopOutlined />, color: '#00B140' },
Miravia: { icon: <ShopOutlined />, color: '#FF69B4' },
Daraz: { icon: <ShopOutlined />, color: '#FF5722' },
Joybuy: { icon: <ShopOutlined />, color: '#E74C3C' },
Alibaba: { icon: <ShopOutlined />, color: '#FF6A00' },
Qoo10: { icon: <ShopOutlined />, color: '#FF4500' },
Shopify: { icon: <ShopOutlined />, color: '#96bf48' },
Shoplazza: { icon: <ShopOutlined />, color: '#3498DB' },
ShopyyV1: { icon: <ShopOutlined />, color: '#9370DB' },
ShopyyV2: { icon: <ShopOutlined />, color: '#9370DB' },
Shopline: { icon: <ShopOutlined />, color: '#27AE60' },
GreatBoss: { icon: <ShopOutlined />, color: '#3498DB' },
Other: { icon: <ShopOutlined />, color: '#999999' },
// 原有平台
eBay: { icon: <GlobalOutlined />, color: '#e53238' },
};
const ProductList: FC = () => {
const navigate = useNavigate();
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 [columnDrawerVisible, setColumnDrawerVisible] = useState(false);
const [currentProduct, setCurrentProduct] = useState<Product | null>(null);
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
const [activePlatformTab, setActivePlatformTab] = useState<string>('all');
const [shops, setShops] = useState<Shop[]>([]);
const [platformStats, setPlatformStats] = useState<Record<string, { total: number; live: number; pending: number; failed: number }>>({
all: { total: 0, live: 0, pending: 0, failed: 0 },
unpublished: { total: 0, live: 0, pending: 0, failed: 0 },
});
const [filters, setFilters] = useState<ProductFilter>({
keyword: '',
status: [],
platform: [],
shop: [],
category: [],
priceRange: undefined,
stockRange: undefined,
roiRange: undefined,
dateRange: undefined,
minProfit: undefined,
minStock: undefined,
searchInDescription: false,
});
const [sort, setSort] = useState<ProductSort>({
field: 'updatedAt',
order: 'descend',
});
const [visibleColumns, setVisibleColumns] = useState<string[]>([
'name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action'
]);
const [columnOrder, setColumnOrder] = useState<string[]>([
'name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action'
]);
useEffect(() => {
fetchInitialData();
}, []);
useEffect(() => {
fetchProducts();
}, [filters, sort, activePlatformTab]);
const fetchInitialData = async () => {
try {
const [shopsData, stats] = await Promise.all([
productDataSource.fetchShops(),
productDataSource.getPlatformStats()
]);
setShops(shopsData);
setPlatformStats(stats);
} catch (error) {
console.error('Failed to fetch initial data:', error);
}
};
const fetchProducts = useCallback(async () => {
setLoading(true);
try {
const productFilter: ProductFilter = {
...filters,
platform: activePlatformTab !== 'all' && activePlatformTab !== 'unpublished'
? [activePlatformTab]
: filters.platform
};
const productsData = await productDataSource.fetchProducts(productFilter, sort);
setProducts(productsData);
// 更新平台统计数据
const stats = await productDataSource.getPlatformStats();
setPlatformStats(stats);
} catch (error) {
console.error('Failed to fetch products:', error);
message.error('Failed to load products');
} finally {
setLoading(false);
}
}, [filters, sort, activePlatformTab]);
const handleFilterChange = (key: keyof ProductFilter, 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: [],
shop: [],
category: [],
priceRange: undefined,
stockRange: undefined,
roiRange: undefined,
dateRange: undefined,
minProfit: undefined,
minStock: undefined,
searchInDescription: false,
});
message.success('筛选条件已重置');
};
const handleColumnVisibilityChange = (columnKey: string, visible: boolean) => {
if (visible) {
setVisibleColumns([...visibleColumns, columnKey]);
} else {
setVisibleColumns(visibleColumns.filter(key => key !== columnKey));
}
};
const handleColumnOrderChange = (newOrder: string[]) => {
setColumnOrder(newOrder);
};
const handleAddProduct = () => {
navigate('/dashboard/product/publish');
};
const handleEditProduct = (record: Product) => {
navigate(`/dashboard/product/detail/${record.id}`);
};
const handleViewProduct = (record: Product) => {
navigate(`/dashboard/product/detail/${record.id}`);
};
const handleDeleteProduct = async (record: Product) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除商品 "${record.name}" 吗?此操作不可恢复。`,
okText: '删除',
okType: 'danger',
onOk: async () => {
try {
const success = await productDataSource.deleteProduct(record.id);
if (success) {
message.success('商品删除成功');
fetchProducts();
} else {
message.error('删除失败');
}
} catch (error) {
message.error('删除失败');
}
},
});
};
const handleDuplicateProduct = async (record: Product) => {
try {
await productDataSource.duplicateProduct(record.id);
message.success('商品复制成功');
fetchProducts();
} catch (error) {
message.error('复制失败');
}
};
const handlePricing = (record: Product) => {
setCurrentProduct(record);
setPricingModalVisible(true);
};
const handlePublish = async (record: Product) => {
Modal.confirm({
title: '确认上架',
content: `确定要上架商品 "${record.name}" 吗?`,
onOk: async () => {
try {
await productDataSource.updateProductStatus(record.id, 'LISTED');
message.success('商品上架成功');
fetchProducts();
} catch (error) {
message.error('上架失败');
}
},
});
};
const handleSync = async (record: Product) => {
setSyncLoading(prev => ({ ...prev, [record.id]: true }));
message.loading({ content: '正在同步到平台...', key: record.id });
try {
await productDataSource.syncProduct(record.id);
message.success({ content: '同步完成', key: record.id });
fetchProducts();
} catch (error) {
message.error({ content: '同步失败,请重试', key: record.id });
} finally {
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
}
};
const handleOffline = async (record: Product) => {
Modal.confirm({
title: '确认下架',
content: `确定要下架商品 "${record.name}" 吗?`,
onOk: async () => {
try {
await productDataSource.updateProductStatus(record.id, 'OFFLINE');
message.success('商品已下架');
fetchProducts();
} catch (error) {
message.error('下架失败');
}
},
});
};
const handleRetrySync = (record: Product) => {
handleSync(record);
};
const handleBatchDelete = () => {
if (selectedRows.length === 0) {
message.warning('请先选择要删除的商品');
return;
}
Modal.confirm({
title: '确认批量删除',
content: `确定要删除选中的 ${selectedRows.length} 个商品吗?`,
okText: '删除',
okType: 'danger',
onOk: async () => {
try {
const promises = selectedRows.map(r => productDataSource.deleteProduct(r.id));
await Promise.all(promises);
message.success(`成功删除 ${selectedRows.length} 个商品`);
setSelectedRows([]);
fetchProducts();
} catch (error) {
message.error('批量删除失败');
}
},
});
};
const handleBatchPricing = () => {
if (selectedRows.length === 0) {
message.warning('请先选择要定价的商品');
return;
}
message.info(`批量定价功能:已选择 ${selectedRows.length} 个商品`);
};
const handleBatchPublish = async () => {
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: async () => {
try {
const promises = pricedProducts.map(p => productDataSource.updateProductStatus(p.id, 'LISTED'));
await Promise.all(promises);
message.success(`成功上架 ${pricedProducts.length} 个商品`);
setSelectedRows([]);
fetchProducts();
} catch (error) {
message.error('批量上架失败');
}
},
});
};
const handleBatchSync = async () => {
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('正在批量同步...');
try {
const promises = listableProducts.map(p => productDataSource.syncProduct(p.id));
await Promise.all(promises);
message.success('批量同步完成');
setSelectedRows([]);
fetchProducts();
} catch (error) {
message.error('批量同步失败');
}
};
const handleBatchUpdate = () => {
if (selectedRows.length === 0) {
message.warning('请先选择要更新的商品');
return;
}
message.info(`批量更新功能:已选择 ${selectedRows.length} 个商品`);
// 这里可以打开批量更新模态框,实现批量更新商品信息
};
const handleBatchExport = () => {
if (selectedRows.length === 0) {
message.warning('请先选择要导出的商品');
return;
}
message.success(`成功导出 ${selectedRows.length} 个商品数据`);
// 这里可以实现导出商品数据为Excel或CSV文件
};
const sortedProducts = products;
const allColumns: ColumnsType<Product> = [
{
title: '商品信息',
dataIndex: 'name',
key: 'name',
render: (text, record) => (
<Space>
<Image src={record.images?.[0] || ''} 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.categories?.[0] || ''}</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
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 columns = columnOrder
.filter(key => visibleColumns.includes(key))
.map(key => allColumns.find(col => col.key === key))
.filter(Boolean) as ColumnsType<Product>;
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 }}
/>
<Tabs
activeKey={activePlatformTab}
onChange={setActivePlatformTab}
tabBarExtraContent={
<Space>
<Button icon={<FilterOutlined />} onClick={() => setFilterVisible(true)}>
</Button>
<Button icon={<SortAscendingOutlined />} onClick={() => setSortDrawerVisible(true)}>
</Button>
<Button icon={<AppstoreOutlined />} onClick={() => setColumnDrawerVisible(true)}>
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddProduct}>
</Button>
</Space>
}
>
<Tabs.TabPane
tab={
<span>
<AppstoreOutlined />
({platformStats.all?.total || 0})
</span>
}
key="all"
/>
{PLATFORMS.map(platform => {
const config = PLATFORM_CONFIG[platform];
const stat = platformStats[platform] || { total: 0, live: 0, pending: 0, failed: 0 };
return (
<Tabs.TabPane
tab={
<span>
{config.icon}
{platform} ({stat.total})
{stat.failed > 0 && (
<Badge count={stat.failed} size="small" style={{ marginLeft: 4 }} />
)}
</span>
}
key={platform}
/>
);
})}
<Tabs.TabPane
tab={
<span>
<ExclamationCircleOutlined />
({platformStats.unpublished?.total || 0})
</span>
}
key="unpublished"
/>
</Tabs>
{activePlatformTab !== 'all' && activePlatformTab !== 'unpublished' && (
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small">
<Statistic
title="已在线"
value={platformStats[activePlatformTab]?.live || 0}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="处理中"
value={platformStats[activePlatformTab]?.pending || 0}
valueStyle={{ color: '#1890ff' }}
prefix={<SyncOutlined spin />}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="同步失败"
value={platformStats[activePlatformTab]?.failed || 0}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="总计"
value={platformStats[activePlatformTab]?.total || 0}
prefix={<ShoppingOutlined />}
/>
</Card>
</Col>
</Row>
)}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={24}>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space>
<Title level={4} style={{ margin: 0 }}>
{activePlatformTab === 'all' ? '全部商品' :
activePlatformTab === 'unpublished' ? '未发布商品' :
`${activePlatformTab} 商品`}
</Title>
<Badge count={sortedProducts.length} showZero style={{ backgroundColor: '#1890ff' }} />
</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" onClick={handleBatchUpdate}>
</Button>
<Button size="small" onClick={handleBatchExport}>
</Button>
<Button size="small" danger onClick={handleBatchDelete}>
</Button>
</Space>
}
style={{ marginBottom: 16 }}
/>
)}
{loading ? (
<LoadingState tip="正在加载商品数据..." />
) : sortedProducts.length === 0 ? (
<EmptyState
title="暂无商品"
description="您还没有添加任何商品,点击下方按钮添加新商品"
showAddButton={true}
onAdd={handleAddProduct}
showReloadButton={true}
onReload={fetchProducts}
/>
) : (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={sortedProducts}
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)}
open={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.shop}
onChange={(value) => handleFilterChange('shop', value)}
style={{ width: '100%' }}
>
{shops.map(shop => (
<Option key={shop.id} value={shop.id}>
{shop.name} ({shop.platform})
</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="价格范围">
<Row gutter={8}>
<Col span={11}>
<InputNumber
min={0}
placeholder="最小"
value={filters.priceRange?.[0] || undefined}
onChange={(value) => {
handleFilterChange('priceRange', filters.priceRange ? [value || 0, filters.priceRange[1]] : [value || 0, 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
<Col span={2} style={{ textAlign: 'center' }}>-
</Col>
<Col span={11}>
<InputNumber
min={0}
placeholder="最大"
value={filters.priceRange?.[1] || undefined}
onChange={(value) => {
handleFilterChange('priceRange', filters.priceRange ? [filters.priceRange[0], value || 999999] : [0, value || 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label="库存范围">
<Row gutter={8}>
<Col span={11}>
<InputNumber
min={0}
placeholder="最小"
value={filters.stockRange?.[0] || undefined}
onChange={(value) => {
handleFilterChange('stockRange', filters.stockRange ? [value || 0, filters.stockRange[1]] : [value || 0, 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
<Col span={2} style={{ textAlign: 'center' }}>-
</Col>
<Col span={11}>
<InputNumber
min={0}
placeholder="最大"
value={filters.stockRange?.[1] || undefined}
onChange={(value) => {
handleFilterChange('stockRange', filters.stockRange ? [filters.stockRange[0], value || 999999] : [0, value || 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label="ROI范围">
<Row gutter={8}>
<Col span={11}>
<InputNumber
min={0}
placeholder="最小"
value={filters.roiRange?.[0] || undefined}
onChange={(value) => {
handleFilterChange('roiRange', filters.roiRange ? [value || 0, filters.roiRange[1]] : [value || 0, 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
<Col span={2} style={{ textAlign: 'center' }}>-
</Col>
<Col span={11}>
<InputNumber
min={0}
placeholder="最大"
value={filters.roiRange?.[1] || undefined}
onChange={(value) => {
handleFilterChange('roiRange', filters.roiRange ? [filters.roiRange[0], value || 999999] : [0, value || 999999]);
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label="最小利润">
<InputNumber
min={0}
placeholder="输入最小利润"
value={filters.minProfit}
onChange={(value) => handleFilterChange('minProfit', value)}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item label="最小库存">
<InputNumber
min={0}
placeholder="输入最小库存"
value={filters.minStock}
onChange={(value) => handleFilterChange('minStock', value)}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item label="更新时间">
<RangePicker
value={filters.dateRange}
onChange={(dates) => handleFilterChange('dateRange', dates)}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={filters.searchInDescription}
onChange={(e) => handleFilterChange('searchInDescription', e.target.checked)}
style={{ marginRight: 8 }}
/>
<label></label>
</div>
</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)}
open={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
icon={<ArrowUpOutlined />}
type={sort.field === item.key && sort.order === 'ascend' ? 'primary' : 'default'}
onClick={() => handleSortChange(item.key, 'ascend')}
>
</Button>
<Button
icon={<ArrowDownOutlined />}
type={sort.field === item.key && sort.order === 'descend' ? 'primary' : 'default'}
onClick={() => handleSortChange(item.key, 'descend')}
>
</Button>
</Space>
</div>
</Card>
))}
</Space>
</Drawer>
<Drawer
title="列设置"
placement="right"
onClose={() => setColumnDrawerVisible(false)}
open={columnDrawerVisible}
width={400}
>
<div style={{ marginBottom: 20 }}>
<h3></h3>
</div>
<Space direction="vertical" style={{ width: '100%', marginBottom: 20 }}>
{
[
{ key: 'name', label: '商品信息' },
{ key: 'price', label: '售价' },
{ key: 'costPrice', label: '成本' },
{ key: 'profit', label: '利润' },
{ key: 'roi', label: 'ROI' },
{ key: 'stock', label: '库存' },
{ key: 'status', label: '状态' },
{ key: 'platformStatus', label: '平台状态' },
{ key: 'updatedAt', label: '更新时间' },
{ key: 'action', label: '操作' },
].map(item => (
<div key={item.key} style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
<input
type="checkbox"
checked={visibleColumns.includes(item.key)}
onChange={(e) => handleColumnVisibilityChange(item.key, e.target.checked)}
style={{ marginRight: 12 }}
/>
<span>{item.label}</span>
</div>
))
}
</Space>
<div style={{ marginBottom: 20 }}>
<h3></h3>
<p style={{ color: '#999', fontSize: 12 }}></p>
</div>
<div style={{ border: '1px dashed #d9d9d9', borderRadius: 4, padding: 16, minHeight: 200 }}>
{columnOrder.map(key => {
const columnInfo = [
{ key: 'name', label: '商品信息' },
{ key: 'price', label: '售价' },
{ key: 'costPrice', label: '成本' },
{ key: 'profit', label: '利润' },
{ key: 'roi', label: 'ROI' },
{ key: 'stock', label: '库存' },
{ key: 'status', label: '状态' },
{ key: 'platformStatus', label: '平台状态' },
{ key: 'updatedAt', label: '更新时间' },
{ key: 'action', label: '操作' },
].find(item => item.key === key);
return (
<div key={key} style={{
padding: 8,
marginBottom: 8,
backgroundColor: '#f0f2f5',
borderRadius: 4,
cursor: 'move',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span>{columnInfo?.label}</span>
<Space size="small">
<Button
size="small"
icon={<ArrowUpOutlined />}
onClick={() => {
const currentIndex = columnOrder.indexOf(key);
if (currentIndex > 0) {
const newOrder = [...columnOrder];
[newOrder[currentIndex], newOrder[currentIndex - 1]] = [newOrder[currentIndex - 1], newOrder[currentIndex]];
handleColumnOrderChange(newOrder);
}
}}
/>
<Button
size="small"
icon={<ArrowDownOutlined />}
onClick={() => {
const currentIndex = columnOrder.indexOf(key);
if (currentIndex < columnOrder.length - 1) {
const newOrder = [...columnOrder];
[newOrder[currentIndex], newOrder[currentIndex + 1]] = [newOrder[currentIndex + 1], newOrder[currentIndex]];
handleColumnOrderChange(newOrder);
}
}}
/>
</Space>
</div>
);
})}
</div>
<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={() => {
setVisibleColumns(['name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action']);
setColumnOrder(['name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action']);
message.success('列设置已重置');
}}></Button>
<Button type="primary" onClick={() => setColumnDrawerVisible(false)}></Button>
</Space>
</div>
</Drawer>
<Modal
title="商品定价"
open={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;