Files
makemd/dashboard/src/pages/Product/ProductList.tsx
wurenzhi 0dac26d781 feat: 添加MSW模拟服务和数据源集成
refactor: 重构页面组件移除冗余Layout组件

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

feat: 添加队列和调度系统

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

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

chore: 更新依赖项配置

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

ci: 配置ESLint边界检查规则

build: 添加Redis和WebSocket依赖

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

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

fix: 修复类型定义和状态管理问题
2026-03-19 01:39:34 +08:00

894 lines
26 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 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;