refactor: 重构项目结构并优化代码
- 删除无用的文件和错误日志 - 创建统一的 imports 模块集中管理依赖 - 重构组件使用新的 imports 方式 - 修复文档路径大小写问题 - 优化类型定义和接口导出 - 更新依赖版本 - 改进错误处理和API配置 - 统一组件导出方式
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useNavigate,
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
@@ -26,8 +29,6 @@ import {
|
||||
Spin,
|
||||
Tabs,
|
||||
Statistic,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
@@ -50,66 +51,27 @@ import {
|
||||
GlobalOutlined,
|
||||
ShopOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
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';
|
||||
|
||||
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>;
|
||||
shopId: string;
|
||||
shopName: string;
|
||||
platform: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Shop {
|
||||
id: string;
|
||||
name: string;
|
||||
platform: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||
region: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
keyword: string;
|
||||
status: string[];
|
||||
platform: string[];
|
||||
shop: string[];
|
||||
category: string[];
|
||||
priceRange: [number, number] | null;
|
||||
stockRange: [number, number] | null;
|
||||
roiRange: [number, number] | null;
|
||||
dateRange: any;
|
||||
minProfit: number | null;
|
||||
minStock: number | null;
|
||||
searchInDescription: boolean;
|
||||
}
|
||||
|
||||
interface SortState {
|
||||
field: string;
|
||||
order: 'ascend' | 'descend' | null;
|
||||
}
|
||||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
DRAFT: { color: 'default', text: '草稿', icon: <EditOutlined /> },
|
||||
@@ -121,86 +83,6 @@ const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.R
|
||||
OFFLINE: { color: 'default', text: '已下架', icon: <ExclamationCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PLATFORMS = [
|
||||
// TikTok系列
|
||||
'TikTok', 'TikTokFull',
|
||||
|
||||
// Shopee系列
|
||||
'Shopee', 'ShopeeFull', 'ShopeeLight',
|
||||
|
||||
// Lazada系列
|
||||
'Lazada', 'LazadaFull',
|
||||
|
||||
// Temu
|
||||
'TemuFull',
|
||||
|
||||
// SHEIN系列
|
||||
'Shein', 'SheinHalf',
|
||||
|
||||
// 其他平台
|
||||
'Ozon', 'Yandex', 'AliExpress', 'AliExpressHalf', 'AliExpressPop',
|
||||
'Coupang', 'Walmart', 'Wildberries', 'Allegro', 'MercadoLibre',
|
||||
'Jumia', 'Joom', 'Amazon', 'Wish', 'Emag', 'Miravia',
|
||||
'Daraz', 'Joybuy', 'Alibaba', 'Qoo10', 'Shopify', 'Shoplazza',
|
||||
'ShopyyV1', 'ShopyyV2', 'Shopline', 'GreatBoss', 'Other',
|
||||
|
||||
// 原有平台
|
||||
'eBay',
|
||||
];
|
||||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||||
|
||||
// 店铺数据
|
||||
const SHOPS: Shop[] = [
|
||||
// TikTok平台 - 多个店铺
|
||||
{ id: 'shop-tiktok-1', name: 'TikTok旗舰店A', platform: 'TikTok', status: 'ACTIVE', region: '中国', currency: 'CNY' },
|
||||
{ id: 'shop-tiktok-2', name: 'TikTok旗舰店B', platform: 'TikTok', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-tiktok-3', name: 'TikTok精品店C', platform: 'TikTok', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-tiktok-4', name: 'TikTok精品店D', platform: 'TikTok', status: 'INACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
|
||||
// Shopee平台 - 多个店铺
|
||||
{ id: 'shop-shopee-1', name: 'Shopee官方店A', platform: 'Shopee', status: 'ACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
{ id: 'shop-shopee-2', name: 'Shopee官方店B', platform: 'Shopee', status: 'ACTIVE', region: '台湾', currency: 'TWD' },
|
||||
{ id: 'shop-shopee-3', name: 'Shopee旗舰店C', platform: 'Shopee', status: 'ACTIVE', region: '马来西亚', currency: 'MYR' },
|
||||
{ id: 'shop-shopee-4', name: 'Shopee旗舰店D', platform: 'Shopee', status: 'SUSPENDED', region: '菲律宾', currency: 'PHP' },
|
||||
|
||||
// Lazada平台 - 多个店铺
|
||||
{ id: 'shop-lazada-1', name: 'Lazada官方店A', platform: 'Lazada', status: 'ACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
{ id: 'shop-lazada-2', name: 'Lazada官方店B', platform: 'Lazada', status: 'ACTIVE', region: '泰国', currency: 'THB' },
|
||||
{ id: 'shop-lazada-3', name: 'Lazada旗舰店C', platform: 'Lazada', status: 'ACTIVE', region: '越南', currency: 'VND' },
|
||||
|
||||
// Amazon平台 - 多个店铺
|
||||
{ id: 'shop-amazon-1', name: 'Amazon美国店A', platform: 'Amazon', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-amazon-2', name: 'Amazon美国店B', platform: 'Amazon', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-amazon-3', name: 'Amazon欧洲店C', platform: 'Amazon', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-amazon-4', name: 'Amazon欧洲店D', platform: 'Amazon', status: 'ACTIVE', region: '英国', currency: 'GBP' },
|
||||
{ id: 'shop-amazon-5', name: 'Amazon日本店E', platform: 'Amazon', status: 'ACTIVE', region: '日本', currency: 'JPY' },
|
||||
|
||||
// eBay平台 - 多个店铺
|
||||
{ id: 'shop-ebay-1', name: 'eBay全球店A', platform: 'eBay', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-ebay-2', name: 'eBay全球店B', platform: 'eBay', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-ebay-3', name: 'eBay美国店C', platform: 'eBay', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
|
||||
// Shopify平台 - 多个店铺
|
||||
{ id: 'shop-shopify-1', name: 'Shopify独立站A', platform: 'Shopify', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-shopify-2', name: 'Shopify独立站B', platform: 'Shopify', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-shopify-3', name: 'Shopify独立站C', platform: 'Shopify', status: 'ACTIVE', region: '亚洲', currency: 'CNY' },
|
||||
|
||||
// Temu平台 - 多个店铺
|
||||
{ id: 'shop-temu-1', name: 'Temu旗舰店A', platform: 'Temu', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-temu-2', name: 'Temu旗舰店B', platform: 'Temu', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-temu-3', name: 'Temu旗舰店C', platform: 'Temu', status: 'ACTIVE', region: '澳大利亚', currency: 'AUD' },
|
||||
|
||||
// SHEIN平台 - 多个店铺
|
||||
{ id: 'shop-shein-1', name: 'SHEIN官方店A', platform: 'Shein', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-shein-2', name: 'SHEIN官方店B', platform: 'Shein', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-shein-3', name: 'SHEIN官方店C', platform: 'Shein', status: 'ACTIVE', region: '中东', currency: 'AED' },
|
||||
|
||||
// 其他平台店铺
|
||||
{ id: 'shop-ozon-1', name: 'Ozon旗舰店A', platform: 'Ozon', status: 'ACTIVE', region: '俄罗斯', currency: 'RUB' },
|
||||
{ id: 'shop-ali-1', name: '速卖通旗舰店A', platform: 'AliExpress', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-walmart-1', name: 'Walmart旗舰店A', platform: 'Walmart', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
];
|
||||
|
||||
const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }> = {
|
||||
// TikTok系列
|
||||
TikTok: { icon: <GlobalOutlined />, color: '#000000' },
|
||||
@@ -255,143 +137,7 @@ const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }>
|
||||
eBay: { icon: <GlobalOutlined />, color: '#e53238' },
|
||||
};
|
||||
|
||||
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' },
|
||||
shopId: 'shop-amazon-1',
|
||||
shopName: 'Amazon美国店',
|
||||
platform: 'Amazon',
|
||||
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: {},
|
||||
shopId: 'shop-tiktok-1',
|
||||
shopName: 'TikTok旗舰店',
|
||||
platform: 'TikTok',
|
||||
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: {},
|
||||
shopId: 'shop-shopee-1',
|
||||
shopName: 'Shopee官方店',
|
||||
platform: 'Shopee',
|
||||
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' },
|
||||
shopId: 'shop-amazon-2',
|
||||
shopName: 'Amazon欧洲店',
|
||||
platform: 'Amazon',
|
||||
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' },
|
||||
shopId: 'shop-ebay-1',
|
||||
shopName: 'eBay全球店',
|
||||
platform: 'eBay',
|
||||
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' },
|
||||
shopId: 'shop-temu-1',
|
||||
shopName: 'Temu旗舰店',
|
||||
platform: 'Temu',
|
||||
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' },
|
||||
shopId: 'shop-shein-1',
|
||||
shopName: 'SHEIN官方店',
|
||||
platform: 'Shein',
|
||||
createdAt: '2025-11-20',
|
||||
updatedAt: '2026-02-28',
|
||||
},
|
||||
];
|
||||
|
||||
export const ProductList: React.FC = () => {
|
||||
const ProductList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -403,23 +149,28 @@ export const ProductList: React.FC = () => {
|
||||
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<FilterState>({
|
||||
const [filters, setFilters] = useState<ProductFilter>({
|
||||
keyword: '',
|
||||
status: [],
|
||||
platform: [],
|
||||
shop: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
priceRange: undefined,
|
||||
stockRange: undefined,
|
||||
roiRange: undefined,
|
||||
dateRange: undefined,
|
||||
minProfit: undefined,
|
||||
minStock: undefined,
|
||||
searchInDescription: false,
|
||||
});
|
||||
|
||||
const [sort, setSort] = useState<SortState>({
|
||||
const [sort, setSort] = useState<ProductSort>({
|
||||
field: 'updatedAt',
|
||||
order: 'descend',
|
||||
});
|
||||
@@ -432,17 +183,51 @@ export const ProductList: React.FC = () => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
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);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setProducts(MOCK_PRODUCTS);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
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 FilterState, value: any) => {
|
||||
const handleFilterChange = (key: keyof ProductFilter, value: any) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
@@ -477,12 +262,12 @@ export const ProductList: React.FC = () => {
|
||||
platform: [],
|
||||
shop: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
priceRange: undefined,
|
||||
stockRange: undefined,
|
||||
roiRange: undefined,
|
||||
dateRange: undefined,
|
||||
minProfit: undefined,
|
||||
minStock: undefined,
|
||||
searchInDescription: false,
|
||||
});
|
||||
message.success('筛选条件已重置');
|
||||
@@ -512,32 +297,36 @@ export const ProductList: React.FC = () => {
|
||||
navigate(`/dashboard/product/detail/${record.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteProduct = (record: Product) => {
|
||||
const handleDeleteProduct = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除商品 "${record.name}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
setProducts(products.filter(p => p.id !== record.id));
|
||||
message.success('商品删除成功');
|
||||
onOk: async () => {
|
||||
try {
|
||||
const success = await productDataSource.deleteProduct(record.id);
|
||||
if (success) {
|
||||
message.success('商品删除成功');
|
||||
fetchProducts();
|
||||
} else {
|
||||
message.error('删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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 handleDuplicateProduct = async (record: Product) => {
|
||||
try {
|
||||
await productDataSource.duplicateProduct(record.id);
|
||||
message.success('商品复制成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePricing = (record: Product) => {
|
||||
@@ -545,13 +334,18 @@ export const ProductList: React.FC = () => {
|
||||
setPricingModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePublish = (record: Product) => {
|
||||
const handlePublish = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认上架',
|
||||
content: `确定要上架商品 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
updateProductStatus(record.id, 'LISTED');
|
||||
message.success('商品上架成功');
|
||||
onOk: async () => {
|
||||
try {
|
||||
await productDataSource.updateProductStatus(record.id, 'LISTED');
|
||||
message.success('商品上架成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('上架失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -559,30 +353,31 @@ export const ProductList: React.FC = () => {
|
||||
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');
|
||||
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 }));
|
||||
}
|
||||
|
||||
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
|
||||
};
|
||||
|
||||
const handleOffline = (record: Product) => {
|
||||
const handleOffline = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认下架',
|
||||
content: `确定要下架商品 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
updateProductStatus(record.id, 'OFFLINE');
|
||||
message.success('商品已下架');
|
||||
onOk: async () => {
|
||||
try {
|
||||
await productDataSource.updateProductStatus(record.id, 'OFFLINE');
|
||||
message.success('商品已下架');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('下架失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -591,12 +386,6 @@ export const ProductList: React.FC = () => {
|
||||
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('请先选择要删除的商品');
|
||||
@@ -607,11 +396,16 @@ export const ProductList: React.FC = () => {
|
||||
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} 个商品`);
|
||||
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('批量删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -624,7 +418,7 @@ export const ProductList: React.FC = () => {
|
||||
message.info(`批量定价功能:已选择 ${selectedRows.length} 个商品`);
|
||||
};
|
||||
|
||||
const handleBatchPublish = () => {
|
||||
const handleBatchPublish = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要上架的商品');
|
||||
return;
|
||||
@@ -637,15 +431,21 @@ export const ProductList: React.FC = () => {
|
||||
Modal.confirm({
|
||||
title: '确认批量上架',
|
||||
content: `确定要上架选中的 ${pricedProducts.length} 个商品吗?`,
|
||||
onOk: () => {
|
||||
pricedProducts.forEach(p => updateProductStatus(p.id, 'LISTED'));
|
||||
setSelectedRows([]);
|
||||
message.success(`成功上架 ${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 = () => {
|
||||
const handleBatchSync = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要同步的商品');
|
||||
return;
|
||||
@@ -656,10 +456,15 @@ export const ProductList: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
message.loading('正在批量同步...');
|
||||
setTimeout(() => {
|
||||
listableProducts.forEach(p => handleSync(p));
|
||||
try {
|
||||
const promises = listableProducts.map(p => productDataSource.syncProduct(p.id));
|
||||
await Promise.all(promises);
|
||||
message.success('批量同步完成');
|
||||
setSelectedRows([]);
|
||||
}, 500);
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('批量同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUpdate = () => {
|
||||
@@ -680,109 +485,7 @@ export const ProductList: React.FC = () => {
|
||||
// 这里可以实现导出商品数据为Excel或CSV文件
|
||||
};
|
||||
|
||||
const filteredProducts = products.filter(product => {
|
||||
if (filters.keyword) {
|
||||
const keywordLower = filters.keyword.toLowerCase();
|
||||
if (!product.name.toLowerCase().includes(keywordLower) &&
|
||||
!product.sku.toLowerCase().includes(keywordLower)) {
|
||||
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;
|
||||
}
|
||||
if (filters.shop.length > 0 && !filters.shop.includes(product.shopId)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.priceRange) {
|
||||
const [min, max] = filters.priceRange;
|
||||
if (product.price < min || product.price > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.stockRange) {
|
||||
const [min, max] = filters.stockRange;
|
||||
if (product.stock < min || product.stock > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.roiRange) {
|
||||
const [min, max] = filters.roiRange;
|
||||
if (product.roi < min || product.roi > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.minProfit !== null && product.profit < filters.minProfit) {
|
||||
return false;
|
||||
}
|
||||
if (filters.minStock !== null && product.stock < filters.minStock) {
|
||||
return false;
|
||||
}
|
||||
if (activePlatformTab !== 'all') {
|
||||
if (activePlatformTab === 'unpublished') {
|
||||
if (Object.keys(product.platformStatus).length > 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!product.platformStatus[activePlatformTab]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const platformStats = useMemo(() => {
|
||||
const stats: Record<string, { total: number; live: number; pending: number; failed: number }> = {
|
||||
all: { total: products.length, live: 0, pending: 0, failed: 0 },
|
||||
unpublished: { total: 0, live: 0, pending: 0, failed: 0 },
|
||||
};
|
||||
|
||||
PLATFORMS.forEach(platform => {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
});
|
||||
|
||||
products.forEach(product => {
|
||||
const platforms = Object.keys(product.platformStatus);
|
||||
if (platforms.length === 0) {
|
||||
stats.unpublished.total++;
|
||||
}
|
||||
platforms.forEach(platform => {
|
||||
if (!stats[platform]) {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
}
|
||||
stats[platform].total++;
|
||||
const status = product.platformStatus[platform];
|
||||
if (status === 'LIVE') {
|
||||
stats[platform].live++;
|
||||
stats.all.live++;
|
||||
} else if (status === 'PENDING' || status === 'SYNCING' || status === 'LISTED') {
|
||||
stats[platform].pending++;
|
||||
stats.all.pending++;
|
||||
} else if (status === 'FAILED' || status === 'SYNC_FAILED') {
|
||||
stats[platform].failed++;
|
||||
stats.all.failed++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [products]);
|
||||
|
||||
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 sortedProducts = products;
|
||||
|
||||
const allColumns: ColumnsType<Product> = [
|
||||
{
|
||||
@@ -1171,7 +874,7 @@ export const ProductList: React.FC = () => {
|
||||
title="筛选条件"
|
||||
placement="right"
|
||||
onClose={() => setFilterVisible(false)}
|
||||
visible={filterVisible}
|
||||
open={filterVisible}
|
||||
width={400}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
@@ -1209,7 +912,7 @@ export const ProductList: React.FC = () => {
|
||||
onChange={(value) => handleFilterChange('shop', value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{SHOPS.map(shop => (
|
||||
{shops.map(shop => (
|
||||
<Option key={shop.id} value={shop.id}>
|
||||
{shop.name} ({shop.platform})
|
||||
</Option>
|
||||
@@ -1362,7 +1065,7 @@ export const ProductList: React.FC = () => {
|
||||
title="排序设置"
|
||||
placement="right"
|
||||
onClose={() => setSortDrawerVisible(false)}
|
||||
visible={sortDrawerVisible}
|
||||
open={sortDrawerVisible}
|
||||
width={300}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
@@ -1403,7 +1106,7 @@ export const ProductList: React.FC = () => {
|
||||
title="列设置"
|
||||
placement="right"
|
||||
onClose={() => setColumnDrawerVisible(false)}
|
||||
visible={columnDrawerVisible}
|
||||
open={columnDrawerVisible}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
@@ -1509,7 +1212,7 @@ export const ProductList: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title="商品定价"
|
||||
visible={pricingModalVisible}
|
||||
open={pricingModalVisible}
|
||||
onCancel={() => setPricingModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
|
||||
Reference in New Issue
Block a user