2026-03-19 01:39:34 +08:00
|
|
|
|
import {
|
2026-03-27 16:56:06 +08:00
|
|
|
|
useState,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
useMemo,
|
|
|
|
|
|
useNavigate,
|
2026-03-30 01:20:57 +08:00
|
|
|
|
FC,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
Card,
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Input,
|
2026-03-20 17:53:46 +08:00
|
|
|
|
InputNumber,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
Select,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Dropdown,
|
|
|
|
|
|
Menu,
|
|
|
|
|
|
Image,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
DatePicker,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Drawer,
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
Spin,
|
2026-03-23 12:41:35 +08:00
|
|
|
|
Tabs,
|
|
|
|
|
|
Statistic,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
SyncOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
FilterOutlined,
|
|
|
|
|
|
SortAscendingOutlined,
|
|
|
|
|
|
SortDescendingOutlined,
|
|
|
|
|
|
MoreOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
CopyOutlined,
|
|
|
|
|
|
ArrowUpOutlined,
|
|
|
|
|
|
ArrowDownOutlined,
|
|
|
|
|
|
DollarOutlined,
|
|
|
|
|
|
ShoppingOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
CloseCircleOutlined,
|
|
|
|
|
|
ExclamationCircleOutlined,
|
2026-03-23 12:41:35 +08:00
|
|
|
|
AmazonOutlined,
|
|
|
|
|
|
GlobalOutlined,
|
|
|
|
|
|
ShopOutlined,
|
|
|
|
|
|
AppstoreOutlined,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
Title,
|
|
|
|
|
|
Text,
|
|
|
|
|
|
Option,
|
|
|
|
|
|
RangePicker,
|
|
|
|
|
|
Search,
|
|
|
|
|
|
ColumnsType,
|
|
|
|
|
|
TablePaginationConfig,
|
|
|
|
|
|
FilterValue,
|
|
|
|
|
|
SorterResult,
|
|
|
|
|
|
TableCurrentDataSource,
|
|
|
|
|
|
moment,
|
|
|
|
|
|
productDataSource,
|
|
|
|
|
|
Product,
|
|
|
|
|
|
Shop,
|
|
|
|
|
|
ProductFilter,
|
|
|
|
|
|
ProductSort,
|
|
|
|
|
|
PLATFORMS,
|
|
|
|
|
|
} from '@/imports';
|
2026-03-23 12:41:35 +08:00
|
|
|
|
import { LoadingState, EmptyState } from '../../components/ui';
|
2026-03-20 17:53:46 +08:00
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
|
|
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 /> },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
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' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-30 01:20:57 +08:00
|
|
|
|
const ProductList: FC = () => {
|
2026-03-20 17:53:46 +08:00
|
|
|
|
const navigate = useNavigate();
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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);
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const [columnDrawerVisible, setColumnDrawerVisible] = useState(false);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const [currentProduct, setCurrentProduct] = useState<Product | null>(null);
|
|
|
|
|
|
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const [activePlatformTab, setActivePlatformTab] = useState<string>('all');
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
});
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const [filters, setFilters] = useState<ProductFilter>({
|
2026-03-19 01:39:34 +08:00
|
|
|
|
keyword: '',
|
|
|
|
|
|
status: [],
|
|
|
|
|
|
platform: [],
|
2026-03-25 13:46:26 +08:00
|
|
|
|
shop: [],
|
2026-03-19 01:39:34 +08:00
|
|
|
|
category: [],
|
2026-03-27 16:56:06 +08:00
|
|
|
|
priceRange: undefined,
|
|
|
|
|
|
stockRange: undefined,
|
|
|
|
|
|
roiRange: undefined,
|
|
|
|
|
|
dateRange: undefined,
|
|
|
|
|
|
minProfit: undefined,
|
|
|
|
|
|
minStock: undefined,
|
2026-03-23 12:41:35 +08:00
|
|
|
|
searchInDescription: false,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const [sort, setSort] = useState<ProductSort>({
|
2026-03-19 01:39:34 +08:00
|
|
|
|
field: 'updatedAt',
|
|
|
|
|
|
order: 'descend',
|
|
|
|
|
|
});
|
2026-03-23 12:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
|
]);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-27 16:56:06 +08:00
|
|
|
|
fetchInitialData();
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const fetchProducts = useCallback(async () => {
|
|
|
|
|
|
setLoading(true);
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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]);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleFilterChange = (key: keyof ProductFilter, value: any) => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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: [],
|
2026-03-25 13:46:26 +08:00
|
|
|
|
shop: [],
|
2026-03-19 01:39:34 +08:00
|
|
|
|
category: [],
|
2026-03-27 16:56:06 +08:00
|
|
|
|
priceRange: undefined,
|
|
|
|
|
|
stockRange: undefined,
|
|
|
|
|
|
roiRange: undefined,
|
|
|
|
|
|
dateRange: undefined,
|
|
|
|
|
|
minProfit: undefined,
|
|
|
|
|
|
minStock: undefined,
|
2026-03-23 12:41:35 +08:00
|
|
|
|
searchInDescription: false,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
message.success('筛选条件已重置');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const handleColumnVisibilityChange = (columnKey: string, visible: boolean) => {
|
|
|
|
|
|
if (visible) {
|
|
|
|
|
|
setVisibleColumns([...visibleColumns, columnKey]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setVisibleColumns(visibleColumns.filter(key => key !== columnKey));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleColumnOrderChange = (newOrder: string[]) => {
|
|
|
|
|
|
setColumnOrder(newOrder);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const handleAddProduct = () => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
navigate('/dashboard/product/publish');
|
2026-03-19 01:39:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleEditProduct = (record: Product) => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
navigate(`/dashboard/product/detail/${record.id}`);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleViewProduct = (record: Product) => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
navigate(`/dashboard/product/detail/${record.id}`);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleDeleteProduct = async (record: Product) => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认删除',
|
|
|
|
|
|
content: `确定要删除商品 "${record.name}" 吗?此操作不可恢复。`,
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
2026-03-27 16:56:06 +08:00
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const success = await productDataSource.deleteProduct(record.id);
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
message.success('商品删除成功');
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleDuplicateProduct = async (record: Product) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await productDataSource.duplicateProduct(record.id);
|
|
|
|
|
|
message.success('商品复制成功');
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('复制失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePricing = (record: Product) => {
|
|
|
|
|
|
setCurrentProduct(record);
|
|
|
|
|
|
setPricingModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handlePublish = async (record: Product) => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认上架',
|
|
|
|
|
|
content: `确定要上架商品 "${record.name}" 吗?`,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await productDataSource.updateProductStatus(record.id, 'LISTED');
|
|
|
|
|
|
message.success('商品上架成功');
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('上架失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSync = async (record: Product) => {
|
|
|
|
|
|
setSyncLoading(prev => ({ ...prev, [record.id]: true }));
|
|
|
|
|
|
|
|
|
|
|
|
message.loading({ content: '正在同步到平台...', key: record.id });
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await productDataSource.syncProduct(record.id);
|
|
|
|
|
|
message.success({ content: '同步完成', key: record.id });
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} catch (error) {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
message.error({ content: '同步失败,请重试', key: record.id });
|
2026-03-27 16:56:06 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleOffline = async (record: Product) => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认下架',
|
|
|
|
|
|
content: `确定要下架商品 "${record.name}" 吗?`,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await productDataSource.updateProductStatus(record.id, 'OFFLINE');
|
|
|
|
|
|
message.success('商品已下架');
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('下架失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRetrySync = (record: Product) => {
|
|
|
|
|
|
handleSync(record);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBatchDelete = () => {
|
|
|
|
|
|
if (selectedRows.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要删除的商品');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认批量删除',
|
|
|
|
|
|
content: `确定要删除选中的 ${selectedRows.length} 个商品吗?`,
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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('批量删除失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBatchPricing = () => {
|
|
|
|
|
|
if (selectedRows.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要定价的商品');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
message.info(`批量定价功能:已选择 ${selectedRows.length} 个商品`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleBatchPublish = async () => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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} 个商品吗?`,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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('批量上架失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const handleBatchSync = async () => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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('正在批量同步...');
|
2026-03-27 16:56:06 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const promises = listableProducts.map(p => productDataSource.syncProduct(p.id));
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
message.success('批量同步完成');
|
2026-03-19 01:39:34 +08:00
|
|
|
|
setSelectedRows([]);
|
2026-03-27 16:56:06 +08:00
|
|
|
|
fetchProducts();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('批量同步失败');
|
|
|
|
|
|
}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
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文件
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const sortedProducts = products;
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const allColumns: ColumnsType<Product> = [
|
2026-03-19 01:39:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '商品信息',
|
|
|
|
|
|
dataIndex: 'name',
|
|
|
|
|
|
key: 'name',
|
|
|
|
|
|
render: (text, record) => (
|
|
|
|
|
|
<Space>
|
2026-03-30 16:51:18 +08:00
|
|
|
|
<Image src={record.images?.[0] || ''} width={60} height={60} style={{ objectFit: 'cover' }} />
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 500 }}>{text}</div>
|
|
|
|
|
|
<div style={{ fontSize: 12, color: '#999' }}>SKU: {record.sku}</div>
|
2026-03-30 16:51:18 +08:00
|
|
|
|
<div style={{ fontSize: 12, color: '#666' }}>{record.categories?.[0] || ''}</div>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const columns = columnOrder
|
|
|
|
|
|
.filter(key => visibleColumns.includes(key))
|
|
|
|
|
|
.map(key => allColumns.find(col => col.key === key))
|
|
|
|
|
|
.filter(Boolean) as ColumnsType<Product>;
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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 }}
|
|
|
|
|
|
/>
|
2026-03-23 12:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Col span={24}>
|
|
|
|
|
|
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
|
|
|
|
<Space>
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
|
|
|
|
{activePlatformTab === 'all' ? '全部商品' :
|
|
|
|
|
|
activePlatformTab === 'unpublished' ? '未发布商品' :
|
|
|
|
|
|
`${activePlatformTab} 商品`}
|
|
|
|
|
|
</Title>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<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>
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<Button size="small" onClick={handleBatchUpdate}>
|
|
|
|
|
|
批量更新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button size="small" onClick={handleBatchExport}>
|
|
|
|
|
|
导出
|
|
|
|
|
|
</Button>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Button size="small" danger onClick={handleBatchDelete}>
|
|
|
|
|
|
批量删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
{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} 条`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
|
|
<Drawer
|
|
|
|
|
|
title="筛选条件"
|
|
|
|
|
|
placement="right"
|
|
|
|
|
|
onClose={() => setFilterVisible(false)}
|
2026-03-27 16:56:06 +08:00
|
|
|
|
open={filterVisible}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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>
|
2026-03-25 13:46:26 +08:00
|
|
|
|
<Form.Item label="店铺">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
mode="multiple"
|
|
|
|
|
|
placeholder="选择店铺"
|
|
|
|
|
|
value={filters.shop}
|
|
|
|
|
|
onChange={(value) => handleFilterChange('shop', value)}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
>
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{shops.map(shop => (
|
2026-03-25 13:46:26 +08:00
|
|
|
|
<Option key={shop.id} value={shop.id}>
|
|
|
|
|
|
{shop.name} ({shop.platform})
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<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>
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<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>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Form.Item label="更新时间">
|
|
|
|
|
|
<RangePicker
|
|
|
|
|
|
value={filters.dateRange}
|
|
|
|
|
|
onChange={(dates) => handleFilterChange('dateRange', dates)}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<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>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</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)}
|
2026-03-27 16:56:06 +08:00
|
|
|
|
open={sortDrawerVisible}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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>
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<Drawer
|
|
|
|
|
|
title="列设置"
|
|
|
|
|
|
placement="right"
|
|
|
|
|
|
onClose={() => setColumnDrawerVisible(false)}
|
2026-03-27 16:56:06 +08:00
|
|
|
|
open={columnDrawerVisible}
|
2026-03-23 12:41:35 +08:00
|
|
|
|
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>
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Modal
|
|
|
|
|
|
title="商品定价"
|
2026-03-27 16:56:06 +08:00
|
|
|
|
open={pricingModalVisible}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
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;
|