feat: 添加MSW模拟服务和数据源集成
refactor: 重构页面组件移除冗余Layout组件 feat: 实现WebSocket和事件总线系统 feat: 添加队列和调度系统 docs: 更新架构文档和服务映射 style: 清理重复接口定义使用数据源 chore: 更新依赖项配置 feat: 添加运行时系统和领域引导 ci: 配置ESLint边界检查规则 build: 添加Redis和WebSocket依赖 test: 添加MSW浏览器环境入口 perf: 优化数据获取逻辑使用统一数据源 fix: 修复类型定义和状态管理问题
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Layout, Typography, Row, Col, Form, Input, Select, Button, Table, Statistic, Spin, message, Alert } from 'antd';
|
||||
import { Card, Typography, Row, Col, Form, Input, Select, Button, Table, Statistic, Spin, message, Alert } from 'antd';
|
||||
import { CalculatorOutlined, SaveOutlined, HistoryOutlined, LineChartOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { productDataSource, AIPricingSuggestion } from '@/services/productDataSource';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Item } = Form;
|
||||
@@ -186,7 +186,7 @@ const AIPricing: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Content style={{ padding: 24, margin: 0, minHeight: 280, background: '#fff' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>AI智能定价</Title>
|
||||
<Button type="primary" icon={<HistoryOutlined />}>
|
||||
@@ -290,7 +290,7 @@ const AIPricing: React.FC = () => {
|
||||
pagination={{ pageSize: 5 }}
|
||||
/>
|
||||
</Card>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
807
dashboard/src/pages/Product/CrossPlatformManage.tsx
Normal file
807
dashboard/src/pages/Product/CrossPlatformManage.tsx
Normal file
@@ -0,0 +1,807 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Input,
|
||||
Select,
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
message,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Alert,
|
||||
Descriptions,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Image,
|
||||
Typography,
|
||||
Tabs,
|
||||
Form,
|
||||
InputNumber,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import {
|
||||
SyncOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
GlobalOutlined,
|
||||
AmazonOutlined,
|
||||
ShoppingOutlined,
|
||||
ShopOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
LinkOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
// ==================== 多商户店铺配置 ====================
|
||||
// 当前用户拥有的店铺(根据登录用户的权限动态加载)
|
||||
interface UserShop {
|
||||
id: string;
|
||||
platform: string;
|
||||
shopId: string;
|
||||
shopName: string;
|
||||
region: string;
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
apiSupported: boolean; // 是否支持API管理
|
||||
}
|
||||
|
||||
// 模拟当前用户拥有的店铺
|
||||
const CURRENT_USER_SHOPS: UserShop[] = [
|
||||
{ id: '1', platform: 'AMAZON', shopId: 'AMZ-US-001', shopName: 'Amazon US Store', region: 'US', status: 'ACTIVE', apiSupported: true },
|
||||
{ id: '2', platform: 'AMAZON', shopId: 'AMZ-EU-001', shopName: 'Amazon EU Store', region: 'EU', status: 'ACTIVE', apiSupported: true },
|
||||
{ id: '3', platform: 'EBAY', shopId: 'EB-US-001', shopName: 'eBay US Store', region: 'US', status: 'ACTIVE', apiSupported: true },
|
||||
{ id: '4', platform: 'SHOPIFY', shopId: 'SF-001', shopName: 'My Shopify Store', region: 'US', status: 'ACTIVE', apiSupported: true },
|
||||
{ id: '5', platform: 'SHOPEE', shopId: 'SP-SG-001', shopName: 'Shopee Singapore', region: 'SG', status: 'ACTIVE', apiSupported: false }, // 不支持API
|
||||
{ id: '6', platform: 'TIKTOK', shopId: 'TK-US-001', shopName: 'TikTok US Shop', region: 'US', status: 'ACTIVE', apiSupported: false }, // 不支持API
|
||||
];
|
||||
|
||||
// 平台配置
|
||||
const PLATFORM_CONFIG: Record<string, { name: string; color: string; icon: React.ReactNode }> = {
|
||||
AMAZON: { name: 'Amazon', color: '#FF9900', icon: <AmazonOutlined /> },
|
||||
EBAY: { name: 'eBay', color: '#E53238', icon: <ShopOutlined /> },
|
||||
SHOPIFY: { name: 'Shopify', color: '#96BF48', icon: <ShoppingOutlined /> },
|
||||
SHOPEE: { name: 'Shopee', color: '#EE4D2D', icon: <ShopOutlined /> },
|
||||
TIKTOK: { name: 'TikTok Shop', color: '#000000', icon: <ShopOutlined /> },
|
||||
LAZADA: { name: 'Lazada', color: '#0F156D', icon: <ShopOutlined /> },
|
||||
};
|
||||
|
||||
// ==================== 跨平台商品接口 ====================
|
||||
interface CrossPlatformProduct {
|
||||
id: string;
|
||||
localProductId: string;
|
||||
sku: string;
|
||||
name: string;
|
||||
image: string;
|
||||
category: string;
|
||||
description: string;
|
||||
platforms: {
|
||||
[shopId: string]: {
|
||||
status: 'LIVE' | 'OFFLINE' | 'PENDING' | 'ERROR' | 'SYNCING' | 'NOT_LISTED';
|
||||
platformProductId: string;
|
||||
platformSku: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
sales: number;
|
||||
listingUrl: string;
|
||||
lastSync: string;
|
||||
shopInfo: UserShop;
|
||||
};
|
||||
};
|
||||
basePrice: number;
|
||||
baseStock: number;
|
||||
totalSales: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ==================== 模拟数据 ====================
|
||||
const MOCK_CROSS_PLATFORM_PRODUCTS: CrossPlatformProduct[] = [
|
||||
{
|
||||
id: 'CP001',
|
||||
localProductId: 'P001',
|
||||
sku: 'TP-TEMP-001',
|
||||
name: '工业温度传感器 Pro',
|
||||
image: 'https://via.placeholder.com/200x200?text=Product',
|
||||
category: '工业自动化',
|
||||
description: '高精度工业温度传感器,适用于各种工业环境,测量范围-40°C至125°C',
|
||||
basePrice: 89.99,
|
||||
baseStock: 256,
|
||||
totalSales: 1250,
|
||||
createdAt: '2025-12-15 10:00:00',
|
||||
updatedAt: '2026-03-18 10:30:00',
|
||||
platforms: {
|
||||
'AMZ-US-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'AMZ-123456',
|
||||
platformSku: 'TP-TEMP-001-AMZ',
|
||||
price: 99.99,
|
||||
stock: 100,
|
||||
sales: 850,
|
||||
listingUrl: 'https://amazon.com/dp/123456',
|
||||
lastSync: '2026-03-18 10:00:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[0],
|
||||
},
|
||||
'AMZ-EU-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'AMZ-EU-789',
|
||||
platformSku: 'TP-TEMP-001-AMZ-EU',
|
||||
price: 89.99,
|
||||
stock: 80,
|
||||
sales: 200,
|
||||
listingUrl: 'https://amazon.de/dp/789',
|
||||
lastSync: '2026-03-18 09:00:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[1],
|
||||
},
|
||||
'EB-US-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'EB-789012',
|
||||
platformSku: 'TP-TEMP-001-EB',
|
||||
price: 95.99,
|
||||
stock: 76,
|
||||
sales: 200,
|
||||
listingUrl: 'https://ebay.com/itm/789012',
|
||||
lastSync: '2026-03-18 09:30:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[2],
|
||||
},
|
||||
'SP-SG-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'SP-345678',
|
||||
platformSku: 'TP-TEMP-001-SP',
|
||||
price: 89.99,
|
||||
stock: 76,
|
||||
sales: 80,
|
||||
listingUrl: 'https://shopee.sg/product/345678',
|
||||
lastSync: '2026-03-18 09:00:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[4],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'CP002',
|
||||
localProductId: 'P002',
|
||||
sku: 'TP-PRES-002',
|
||||
name: '压力传感器 Digital',
|
||||
image: 'https://via.placeholder.com/200x200?text=Product',
|
||||
category: '工业自动化',
|
||||
description: '数字式压力传感器,精度0.1%,支持多种输出信号',
|
||||
basePrice: 129.99,
|
||||
baseStock: 128,
|
||||
totalSales: 680,
|
||||
createdAt: '2026-03-10 14:30:00',
|
||||
updatedAt: '2026-03-18 09:15:00',
|
||||
platforms: {
|
||||
'AMZ-US-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'AMZ-234567',
|
||||
platformSku: 'TP-PRES-002-AMZ',
|
||||
price: 139.99,
|
||||
stock: 50,
|
||||
sales: 500,
|
||||
listingUrl: 'https://amazon.com/dp/234567',
|
||||
lastSync: '2026-03-18 10:00:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[0],
|
||||
},
|
||||
'SF-001': {
|
||||
status: 'LIVE',
|
||||
platformProductId: 'SF-890123',
|
||||
platformSku: 'TP-PRES-002-SF',
|
||||
price: 129.99,
|
||||
stock: 78,
|
||||
sales: 180,
|
||||
listingUrl: 'https://mystore.myshopify.com/products/890123',
|
||||
lastSync: '2026-03-18 08:30:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[3],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'CP003',
|
||||
localProductId: 'P003',
|
||||
sku: 'TP-FLOW-003',
|
||||
name: '流量计 Ultra',
|
||||
image: 'https://via.placeholder.com/200x200?text=Product',
|
||||
category: '仪器仪表',
|
||||
description: '超声波流量计,非接触式测量,精度高',
|
||||
basePrice: 299.99,
|
||||
baseStock: 64,
|
||||
totalSales: 320,
|
||||
createdAt: '2026-03-01 09:00:00',
|
||||
updatedAt: '2026-03-17 16:45:00',
|
||||
platforms: {
|
||||
'AMZ-US-001': {
|
||||
status: 'OFFLINE',
|
||||
platformProductId: 'AMZ-345678',
|
||||
platformSku: 'TP-FLOW-003-AMZ',
|
||||
price: 319.99,
|
||||
stock: 0,
|
||||
sales: 300,
|
||||
listingUrl: 'https://amazon.com/dp/345678',
|
||||
lastSync: '2026-03-17 16:00:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[0],
|
||||
},
|
||||
'EB-US-001': {
|
||||
status: 'ERROR',
|
||||
platformProductId: 'EB-901234',
|
||||
platformSku: 'TP-FLOW-003-EB',
|
||||
price: 299.99,
|
||||
stock: 64,
|
||||
sales: 20,
|
||||
listingUrl: 'https://ebay.com/itm/901234',
|
||||
lastSync: '2026-03-17 15:30:00',
|
||||
shopInfo: CURRENT_USER_SHOPS[2],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
LIVE: { color: 'success', text: '在线', icon: <CheckCircleOutlined /> },
|
||||
OFFLINE: { color: 'default', text: '已下架', icon: <CloseCircleOutlined /> },
|
||||
PENDING: { color: 'warning', text: '审核中', icon: <ExclamationCircleOutlined /> },
|
||||
ERROR: { color: 'error', text: '异常', icon: <ExclamationCircleOutlined /> },
|
||||
SYNCING: { color: 'processing', text: '同步中', icon: <SyncOutlined spin /> },
|
||||
NOT_LISTED: { color: 'default', text: '未上架', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const CrossPlatformManage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [products, setProducts] = useState<CrossPlatformProduct[]>([]);
|
||||
const [filteredProducts, setFilteredProducts] = useState<CrossPlatformProduct[]>([]);
|
||||
const [selectedShop, setSelectedShop] = useState<string>('ALL');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedRows, setSelectedRows] = useState<CrossPlatformProduct[]>([]);
|
||||
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 详情弹窗
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<CrossPlatformProduct | null>(null);
|
||||
|
||||
// 同步弹窗
|
||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||
const [syncForm] = Form.useForm();
|
||||
const [syncingProduct, setSyncingProduct] = useState<CrossPlatformProduct | null>(null);
|
||||
const [selectedSyncShops, setSelectedSyncShops] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterProducts();
|
||||
}, [products, selectedShop, searchText]);
|
||||
|
||||
const fetchProducts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setProducts(MOCK_CROSS_PLATFORM_PRODUCTS);
|
||||
} catch (error) {
|
||||
message.error('加载跨平台商品失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filterProducts = () => {
|
||||
let result = [...products];
|
||||
|
||||
// 按店铺筛选
|
||||
if (selectedShop !== 'ALL') {
|
||||
result = result.filter(p =>
|
||||
p.platforms[selectedShop] &&
|
||||
p.platforms[selectedShop].status !== 'NOT_LISTED'
|
||||
);
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchText) {
|
||||
result = result.filter(p =>
|
||||
p.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
p.sku.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredProducts(result);
|
||||
};
|
||||
|
||||
// 获取用户拥有的店铺列表(按平台分组)
|
||||
const getUserShopsByPlatform = () => {
|
||||
const grouped: Record<string, UserShop[]> = {};
|
||||
CURRENT_USER_SHOPS.forEach(shop => {
|
||||
if (!grouped[shop.platform]) {
|
||||
grouped[shop.platform] = [];
|
||||
}
|
||||
grouped[shop.platform].push(shop);
|
||||
});
|
||||
return grouped;
|
||||
};
|
||||
|
||||
// 查看商品详情
|
||||
const handleViewDetail = (product: CrossPlatformProduct) => {
|
||||
setSelectedProduct(product);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开同步弹窗
|
||||
const handleOpenSyncModal = (product: CrossPlatformProduct) => {
|
||||
setSyncingProduct(product);
|
||||
// 默认选中已上架的店铺
|
||||
const listedShops = Object.entries(product.platforms)
|
||||
.filter(([_, info]) => info.status !== 'NOT_LISTED')
|
||||
.filter(([shopId, info]) => info.shopInfo.apiSupported) // 只选中支持API的店铺
|
||||
.map(([shopId]) => shopId);
|
||||
setSelectedSyncShops(listedShops);
|
||||
syncForm.setFieldsValue({
|
||||
shops: listedShops,
|
||||
syncStock: true,
|
||||
syncPrice: false,
|
||||
});
|
||||
setSyncModalVisible(true);
|
||||
};
|
||||
|
||||
// 执行同步
|
||||
const handleSync = async () => {
|
||||
if (selectedSyncShops.length === 0) {
|
||||
message.warning('请至少选择一个目标店铺');
|
||||
return;
|
||||
}
|
||||
|
||||
const values = syncForm.getFieldsValue();
|
||||
message.loading(`正在同步到 ${selectedSyncShops.length} 个店铺...`, 2);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
message.success(`同步完成!库存: ${values.syncStock ? '是' : '否'}, 价格: ${values.syncPrice ? '是' : '否'}`);
|
||||
setSyncModalVisible(false);
|
||||
fetchProducts();
|
||||
};
|
||||
|
||||
// 批量同步库存
|
||||
const handleBatchSyncStock = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择商品');
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '批量同步库存',
|
||||
content: `将对 ${selectedRows.length} 个商品同步库存到所有支持API的店铺`,
|
||||
onOk: async () => {
|
||||
message.loading('正在批量同步库存...', 2);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
message.success('批量同步完成');
|
||||
fetchProducts();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 获取店铺统计
|
||||
const getShopStats = () => {
|
||||
const stats: Record<string, number> = { ALL: products.length };
|
||||
CURRENT_USER_SHOPS.forEach(shop => {
|
||||
stats[shop.shopId] = products.filter(p =>
|
||||
p.platforms[shop.shopId] && p.platforms[shop.shopId].status !== 'NOT_LISTED'
|
||||
).length;
|
||||
});
|
||||
return stats;
|
||||
};
|
||||
|
||||
const shopStats = getShopStats();
|
||||
const userShopsByPlatform = getUserShopsByPlatform();
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<CrossPlatformProduct> = [
|
||||
{
|
||||
title: '商品信息',
|
||||
key: 'productInfo',
|
||||
width: 300,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Image src={record.image} width={60} height={60} style={{ objectFit: 'cover' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{record.name}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>SKU: {record.sku}</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>{record.category}</div>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '本地信息',
|
||||
key: 'localInfo',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div>价格: ${record.basePrice.toFixed(2)}</div>
|
||||
<div>库存: {record.baseStock}</div>
|
||||
<div>总销量: {record.totalSales}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// 为每个用户店铺生成一列
|
||||
...CURRENT_USER_SHOPS.map(shop => ({
|
||||
title: (
|
||||
<Tooltip title={`${shop.shopName} (${shop.region})`}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ color: PLATFORM_CONFIG[shop.platform]?.color }}>
|
||||
{PLATFORM_CONFIG[shop.platform]?.icon}
|
||||
</div>
|
||||
<div style={{ fontSize: 10 }}>{shop.region}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
key: `shop-${shop.shopId}`,
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: CrossPlatformProduct) => {
|
||||
const platformInfo = record.platforms[shop.shopId];
|
||||
if (!platformInfo || platformInfo.status === 'NOT_LISTED') {
|
||||
return <Tag size="small" style={{ opacity: 0.5 }}>未上架</Tag>;
|
||||
}
|
||||
|
||||
const statusConfig = STATUS_CONFIG[platformInfo.status];
|
||||
const canManage = shop.apiSupported;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item onClick={() => handleViewDetail(record)}>
|
||||
<EyeOutlined /> 查看详情
|
||||
</Menu.Item>
|
||||
{canManage && platformInfo.status === 'LIVE' && (
|
||||
<Menu.Item onClick={() => handleOpenSyncModal(record)}>
|
||||
<SyncOutlined /> 同步库存/价格
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canManage && platformInfo.status === 'LIVE' && (
|
||||
<Menu.Item onClick={() => message.info('打开价格调整')}>
|
||||
<EditOutlined /> 调整价格
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canManage && platformInfo.status === 'LIVE' && (
|
||||
<Menu.Item danger onClick={() => message.info('执行下架')}>
|
||||
<CloseCircleOutlined /> 下架
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canManage && platformInfo.status === 'OFFLINE' && (
|
||||
<Menu.Item onClick={() => message.info('执行上架')}>
|
||||
<CheckCircleOutlined /> 上架
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!canManage && (
|
||||
<Menu.Item disabled>
|
||||
<ExclamationCircleOutlined /> 不支持API管理
|
||||
</Menu.Item>
|
||||
)}
|
||||
{platformInfo.listingUrl && (
|
||||
<Menu.Item>
|
||||
<a href={platformInfo.listingUrl} target="_blank" rel="noopener noreferrer">
|
||||
<LinkOutlined /> 打开商品页
|
||||
</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Tag
|
||||
color={statusConfig.color}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
opacity: canManage ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
{statusConfig.text}
|
||||
{!canManage && <span style={{ fontSize: 10 }}>*</span>}
|
||||
</Tag>
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
})),
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
||||
详情
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<SyncOutlined />} onClick={() => handleOpenSyncModal(record)}>
|
||||
同步
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="cross-platform-manage">
|
||||
<Alert
|
||||
message="跨平台商品管理说明"
|
||||
description={
|
||||
<div>
|
||||
<p>• 集中管理您所有店铺的在线商品</p>
|
||||
<p>• 标记 * 的店铺暂不支持API管理,需要手动操作</p>
|
||||
<p>• 同步操作需要指定目标店铺,只同步到支持API的店铺</p>
|
||||
<p>• 当前拥有 {CURRENT_USER_SHOPS.length} 个店铺</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<Space style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Space>
|
||||
<Title level={5} style={{ margin: 0 }}>跨平台商品列表</Title>
|
||||
<Badge count={filteredProducts.length} showZero style={{ backgroundColor: '#1890ff' }} />
|
||||
</Space>
|
||||
<Space>
|
||||
<Search
|
||||
placeholder="搜索商品名称或SKU"
|
||||
allowClear
|
||||
onSearch={setSearchText}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
<Select
|
||||
value={selectedShop}
|
||||
onChange={setSelectedShop}
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择店铺"
|
||||
>
|
||||
<Option value="ALL">全部店铺 ({shopStats.ALL})</Option>
|
||||
{Object.entries(userShopsByPlatform).map(([platform, shops]) => (
|
||||
<OptGroup key={platform} label={PLATFORM_CONFIG[platform]?.name || platform}>
|
||||
{shops.map(shop => (
|
||||
<Option key={shop.shopId} value={shop.shopId}>
|
||||
<Space>
|
||||
{shop.shopName}
|
||||
{!shop.apiSupported && <Tag size="small" color="warning">No API</Tag>}
|
||||
<span style={{ color: '#999' }}>({shopStats[shop.shopId] || 0})</span>
|
||||
</Space>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{selectedRows.length > 0 && (
|
||||
<Alert
|
||||
message={`已选择 ${selectedRows.length} 个商品`}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
action={
|
||||
<Space>
|
||||
<Button size="small" onClick={handleBatchSyncStock}>
|
||||
<SyncOutlined /> 批量同步库存
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredProducts}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1500 }}
|
||||
pagination={{ showSizeChanger: true, showTotal: (total) => `共 ${total} 个商品` }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRows.map(r => r.id),
|
||||
onChange: (_, rows) => setSelectedRows(rows),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 商品详情弹窗 */}
|
||||
<Modal
|
||||
title="商品详情"
|
||||
visible={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
width={900}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{selectedProduct && (
|
||||
<Tabs defaultActiveKey="info">
|
||||
<TabPane tab="基本信息" key="info">
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Image src={selectedProduct.image} style={{ width: '100%' }} />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="商品名称">{selectedProduct.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="SKU">{selectedProduct.sku}</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">{selectedProduct.category}</Descriptions.Item>
|
||||
<Descriptions.Item label="本地价格">${selectedProduct.basePrice.toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="本地库存">{selectedProduct.baseStock}</Descriptions.Item>
|
||||
<Descriptions.Item label="总销量">{selectedProduct.totalSales}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{selectedProduct.createdAt}</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{selectedProduct.updatedAt}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<div>
|
||||
<Text strong>商品描述:</Text>
|
||||
<p>{selectedProduct.description}</p>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
<TabPane tab="店铺分布" key="shops">
|
||||
<Table
|
||||
dataSource={Object.entries(selectedProduct.platforms)}
|
||||
rowKey="[0]"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: '店铺',
|
||||
render: ([shopId, info]: [string, any]) => (
|
||||
<Space>
|
||||
{PLATFORM_CONFIG[info.shopInfo.platform]?.icon}
|
||||
<span>{info.shopInfo.shopName}</span>
|
||||
<Tag size="small">{info.shopInfo.region}</Tag>
|
||||
{!info.shopInfo.apiSupported && <Tag size="small" color="warning">No API</Tag>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
render: ([_, info]: [string, any]) => (
|
||||
<Tag color={STATUS_CONFIG[info.status].color}>
|
||||
{STATUS_CONFIG[info.status].text}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台SKU',
|
||||
render: ([_, info]: [string, any]) => info.platformSku,
|
||||
},
|
||||
{
|
||||
title: '售价',
|
||||
render: ([_, info]: [string, any]) => `$${info.price.toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
render: ([_, info]: [string, any]) => info.stock,
|
||||
},
|
||||
{
|
||||
title: '销量',
|
||||
render: ([_, info]: [string, any]) => info.sales,
|
||||
},
|
||||
{
|
||||
title: '最后同步',
|
||||
render: ([_, info]: [string, any]) => info.lastSync,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: ([shopId, info]: [string, any]) => (
|
||||
<Space>
|
||||
{info.shopInfo.apiSupported && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleOpenSyncModal(selectedProduct)}
|
||||
>
|
||||
同步
|
||||
</Button>
|
||||
)}
|
||||
{info.listingUrl && (
|
||||
<a href={info.listingUrl} target="_blank" rel="noopener noreferrer">
|
||||
<LinkOutlined /> 查看
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 同步弹窗 */}
|
||||
<Modal
|
||||
title="同步商品"
|
||||
visible={syncModalVisible}
|
||||
onCancel={() => setSyncModalVisible(false)}
|
||||
onOk={handleSync}
|
||||
width={600}
|
||||
>
|
||||
{syncingProduct && (
|
||||
<Form form={syncForm} layout="vertical">
|
||||
<Alert
|
||||
message={`正在同步: ${syncingProduct.name}`}
|
||||
description={`SKU: ${syncingProduct.sku}`}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="shops"
|
||||
label="选择目标店铺"
|
||||
rules={[{ required: true, message: '请至少选择一个店铺' }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择要同步的店铺"
|
||||
onChange={setSelectedSyncShops}
|
||||
>
|
||||
{Object.entries(syncingProduct.platforms)
|
||||
.filter(([_, info]) => info.status !== 'NOT_LISTED')
|
||||
.filter(([_, info]) => info.shopInfo.apiSupported)
|
||||
.map(([shopId, info]) => (
|
||||
<Option key={shopId} value={shopId}>
|
||||
<Space>
|
||||
{PLATFORM_CONFIG[info.shopInfo.platform]?.icon}
|
||||
{info.shopInfo.shopName}
|
||||
<Tag size="small">{info.shopInfo.region}</Tag>
|
||||
</Space>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="syncStock"
|
||||
label="同步库存"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="syncPrice"
|
||||
label="同步价格"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message="同步说明"
|
||||
description="只支持同步到拥有API权限的店铺。不支持API的店铺需要手动操作。"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 添加 OptGroup 导入
|
||||
const { OptGroup } = Select;
|
||||
|
||||
export default CrossPlatformManage;
|
||||
893
dashboard/src/pages/Product/ProductList.tsx
Normal file
893
dashboard/src/pages/Product/ProductList.tsx
Normal file
@@ -0,0 +1,893 @@
|
||||
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;
|
||||
@@ -131,17 +131,46 @@ export const ProductPublishForm: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
// Step 1: 先保存到本地系统
|
||||
const productData = {
|
||||
...values,
|
||||
variants,
|
||||
images: fileList.map(f => f.url || f.thumbUrl),
|
||||
createdAt: new Date().toISOString(),
|
||||
syncStatus: 'PENDING', // 待同步状态
|
||||
};
|
||||
|
||||
// 模拟保存到本地数据库
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
message.success('Product saved to local system');
|
||||
|
||||
// Step 2: 合规检查(本地进行)
|
||||
if (values.complianceCheck) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
message.info('Running compliance check...');
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Step 3: 提交审核(本地状态变更)
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
|
||||
Modal.success({
|
||||
title: 'Product Submitted',
|
||||
content: `Product "${values.name}" has been submitted for ${values.autoPublish ? 'auto-publish' : 'review'} on ${values.platform.join(', ')}.`,
|
||||
title: 'Product Created Successfully',
|
||||
content: (
|
||||
<div>
|
||||
<p>Product "{values.name}" has been saved to local system.</p>
|
||||
<p>Target platforms: {values.platform.join(', ')}</p>
|
||||
<p>Status: {values.autoPublish ? 'Will be auto-published after review' : 'Pending review'}</p>
|
||||
<p style={{ color: '#1890ff', marginTop: 8 }}>
|
||||
The product will be synced to platforms according to the sync schedule.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
form.resetFields();
|
||||
setVariants([]);
|
||||
setFileList([]);
|
||||
} catch (error) {
|
||||
message.error('Please fill in required fields');
|
||||
} finally {
|
||||
@@ -434,9 +463,10 @@ export const ProductPublishForm: React.FC = () => {
|
||||
message="Publishing Rules"
|
||||
description={
|
||||
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
|
||||
<li>All products must pass compliance check before publishing</li>
|
||||
<li>Products are first saved to local system, then synced to platforms</li>
|
||||
<li>All products must pass compliance check before syncing</li>
|
||||
<li>Products with profit margin below threshold will require manual approval</li>
|
||||
<li>Images must meet platform-specific requirements</li>
|
||||
<li>Platform sync happens according to configured schedule</li>
|
||||
</ul>
|
||||
}
|
||||
type="warning"
|
||||
@@ -453,7 +483,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
Save Draft
|
||||
</Button>
|
||||
<Button type="primary" icon={<SendOutlined />} onClick={handleSubmit} loading={loading}>
|
||||
Submit for Review
|
||||
Save to System
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Layout, Typography, Row, Col, Select, Button, Table, Statistic, Spin, message, Alert, Badge } from 'antd';
|
||||
import { Card, Typography, Row, Col, Select, Button, Table, Statistic, Spin, message, Alert, Badge } from 'antd';
|
||||
import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import { AlertOutlined, TrendingUpOutlined, TrendingDownOutlined, ReloadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { productDataSource, ProfitMonitor as ProfitMonitorData } from '@/services/productDataSource';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -232,7 +232,7 @@ const ProfitMonitor: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Content style={{ padding: 24, margin: 0, minHeight: 280, background: '#fff' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>商品利润监控</Title>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
@@ -353,7 +353,7 @@ const ProfitMonitor: React.FC = () => {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Layout, Typography, Row, Col, DatePicker, Select, Button, Table, Statistic, Spin, message } from 'antd';
|
||||
import { Card, Typography, Row, Col, DatePicker, Select, Button, Table, Statistic, Spin, message } from 'antd';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line } from 'recharts';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { productDataSource, ROIAnalysis as ROIAnalysisData } from '@/services/productDataSource';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
@@ -206,7 +206,7 @@ const ROIAnalysis: React.FC = () => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Content style={{ padding: 24, margin: 0, minHeight: 280, background: '#fff' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>商品ROI分析</Title>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
@@ -318,7 +318,7 @@ const ROIAnalysis: React.FC = () => {
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,38 +1,641 @@
|
||||
import React from 'react';
|
||||
import { Card, Layout, Typography, Row, Col, Button } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Button, Table, Input, Select, Modal, Form, InputNumber, message, Tag, Space, Popconfirm, Tabs } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, EditOutlined, SearchOutlined, FilterOutlined, SyncOutlined, DeleteOutlined, EyeOutlined, GlobalOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import CrossPlatformManage from './CrossPlatformManage';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
price: number;
|
||||
cost: number;
|
||||
stock: number;
|
||||
status: 'draft' | 'published' | 'synced';
|
||||
platform: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const ProductManagement: React.FC = () => {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string>('');
|
||||
const [sortField, setSortField] = useState<string>('');
|
||||
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend'>('ascend');
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||
const [viewingProduct, setViewingProduct] = useState<Product | null>(null);
|
||||
const [isPricingModalVisible, setIsPricingModalVisible] = useState(false);
|
||||
const [pricingProduct, setPricingProduct] = useState<Product | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('local');
|
||||
const [form] = Form.useForm();
|
||||
const [pricingForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
const fetchProducts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/products');
|
||||
const data = await response.json();
|
||||
setProducts(data.data || []);
|
||||
} catch (error) {
|
||||
message.error('获取商品列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const handleFilter = (status: string) => {
|
||||
setFilterStatus(status);
|
||||
};
|
||||
|
||||
const handleSort = (field: string, order: 'ascend' | 'descend') => {
|
||||
setSortField(field);
|
||||
setSortOrder(order);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingProduct(null);
|
||||
form.resetFields();
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: Product) => {
|
||||
setEditingProduct(record);
|
||||
form.setFieldsValue(record);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (record: Product) => {
|
||||
setViewingProduct(record);
|
||||
setIsDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePricing = (record: Product) => {
|
||||
setPricingProduct(record);
|
||||
pricingForm.setFieldsValue({
|
||||
cost: record.cost,
|
||||
price: record.price,
|
||||
profit: record.price - record.cost,
|
||||
profitMargin: ((record.price - record.cost) / record.price * 100).toFixed(2)
|
||||
});
|
||||
setIsPricingModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePublish = async (record: Product) => {
|
||||
try {
|
||||
await fetch(`/api/v1/products/${record.id}/publish`, {
|
||||
method: 'POST'
|
||||
});
|
||||
message.success('商品上架成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('商品上架失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async (record: Product) => {
|
||||
try {
|
||||
await fetch(`/api/v1/products/${record.id}/sync`, {
|
||||
method: 'POST'
|
||||
});
|
||||
message.success('商品同步成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('商品同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchSync = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要同步的商品');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetch('/api/v1/products/batch-sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: selectedRowKeys })
|
||||
});
|
||||
message.success('批量同步成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('批量同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await fetch(`/api/v1/products/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success('商品删除成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('商品删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editingProduct) {
|
||||
await fetch(`/api/v1/products/${editingProduct.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
});
|
||||
message.success('商品更新成功');
|
||||
} else {
|
||||
await fetch('/api/v1/products', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
});
|
||||
message.success('商品创建成功');
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePricingChange = () => {
|
||||
const cost = pricingForm.getFieldValue('cost');
|
||||
const price = pricingForm.getFieldValue('price');
|
||||
if (cost && price) {
|
||||
pricingForm.setFieldsValue({
|
||||
profit: price - cost,
|
||||
profitMargin: ((price - cost) / price * 100).toFixed(2)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePricingModalOk = async () => {
|
||||
try {
|
||||
const values = await pricingForm.validateFields();
|
||||
if (pricingProduct) {
|
||||
await fetch(`/api/v1/products/${pricingProduct.id}/pricing`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
});
|
||||
message.success('定价更新成功');
|
||||
setIsPricingModalVisible(false);
|
||||
fetchProducts();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('定价更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredProducts = products.filter(product => {
|
||||
if (searchText && !product.name.toLowerCase().includes(searchText.toLowerCase()) &&
|
||||
!product.sku.toLowerCase().includes(searchText.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (filterStatus && product.status !== filterStatus) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const sortedProducts = [...filteredProducts].sort((a, b) => {
|
||||
const field = sortField as keyof Product;
|
||||
const aValue = a[field];
|
||||
const bValue = b[field];
|
||||
|
||||
if (sortOrder === 'ascend') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '商品名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'sku',
|
||||
key: 'sku'
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
sorter: true,
|
||||
render: (price: number) => `¥${price.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
title: '成本',
|
||||
dataIndex: 'cost',
|
||||
key: 'cost',
|
||||
sorter: true,
|
||||
render: (cost: number) => `¥${cost.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
key: 'stock',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [
|
||||
{ text: '草稿', value: 'draft' },
|
||||
{ text: '已定价', value: 'priced' },
|
||||
{ text: '已上架', value: 'listed' },
|
||||
{ text: '同步中', value: 'syncing' },
|
||||
{ text: '已在线', value: 'live' },
|
||||
{ text: '同步失败', value: 'sync_failed' },
|
||||
{ text: '已下架', value: 'offline' }
|
||||
],
|
||||
render: (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
draft: 'default',
|
||||
priced: 'processing',
|
||||
listed: 'warning',
|
||||
syncing: 'processing',
|
||||
live: 'success',
|
||||
sync_failed: 'error',
|
||||
offline: 'default'
|
||||
};
|
||||
const textMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
priced: '已定价',
|
||||
listed: '已上架(本地)',
|
||||
syncing: '同步中',
|
||||
live: '已在线',
|
||||
sync_failed: '同步失败',
|
||||
offline: '已下架'
|
||||
};
|
||||
return <Tag color={colorMap[status]}>{textMap[status]}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: Product) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handlePricing(record)}
|
||||
>
|
||||
定价
|
||||
</Button>
|
||||
{record.status === 'draft' && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handlePublish(record)}
|
||||
>
|
||||
上架
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => handleSync(record)}
|
||||
>
|
||||
同步
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个商品吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Content style={{ padding: 24, margin: 0, minHeight: 280, background: '#fff' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>商品管理</Title>
|
||||
<div>
|
||||
<Button type="primary" icon={<PlusOutlined />} style={{ marginRight: 8 }}>
|
||||
<Link to="/Product/ProductPublishForm">发布商品</Link>
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增商品
|
||||
</Button>
|
||||
<Button icon={<UploadOutlined />} style={{ marginRight: 8 }}>
|
||||
<Button icon={<UploadOutlined />}>
|
||||
<Link to="/Product/MaterialUpload">素材上传</Link>
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />}>
|
||||
<Link to="/Product/ProductDetail">编辑商品</Link>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleBatchSync}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量同步
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Card title="商品列表">
|
||||
<Text>商品管理页面内容</Text>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} style={{ marginBottom: 16 }}>
|
||||
<Tabs.TabPane tab="本地商品管理" key="local">
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space style={{ width: '100%', marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="搜索商品名称或SKU"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={e => handleSearch(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: 150 }}
|
||||
onChange={handleFilter}
|
||||
allowClear
|
||||
>
|
||||
<Select.Option value="draft">草稿</Select.Option>
|
||||
<Select.Option value="published">已发布</Select.Option>
|
||||
<Select.Option value="synced">已同步</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="排序字段"
|
||||
style={{ width: 150 }}
|
||||
onChange={(value) => handleSort(value, sortOrder)}
|
||||
allowClear
|
||||
>
|
||||
<Select.Option value="name">商品名称</Select.Option>
|
||||
<Select.Option value="price">价格</Select.Option>
|
||||
<Select.Option value="stock">库存</Select.Option>
|
||||
<Select.Option value="createdAt">创建时间</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="排序方式"
|
||||
style={{ width: 120 }}
|
||||
onChange={(value) => handleSort(sortField, value)}
|
||||
value={sortOrder}
|
||||
>
|
||||
<Select.Option value="ascend">升序</Select.Option>
|
||||
<Select.Option value="descend">降序</Select.Option>
|
||||
</Select>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Content>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={sortedProducts}
|
||||
loading={loading}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
跨平台商品管理
|
||||
</span>
|
||||
}
|
||||
key="crossPlatform"
|
||||
>
|
||||
<CrossPlatformManage />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Modal
|
||||
title={editingProduct ? '编辑商品' : '新增商品'}
|
||||
visible={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="商品名称"
|
||||
rules={[{ required: true, message: '请输入商品名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入商品名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sku"
|
||||
label="SKU"
|
||||
rules={[{ required: true, message: '请输入SKU' }]}
|
||||
>
|
||||
<Input placeholder="请输入SKU" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="价格"
|
||||
rules={[{ required: true, message: '请输入价格' }]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入价格"
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="cost"
|
||||
label="成本"
|
||||
rules={[{ required: true, message: '请输入成本' }]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入成本"
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="stock"
|
||||
label="库存"
|
||||
rules={[{ required: true, message: '请输入库存' }]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入库存"
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="platform"
|
||||
label="平台"
|
||||
rules={[{ required: true, message: '请选择平台' }]}
|
||||
>
|
||||
<Select placeholder="请选择平台">
|
||||
<Select.Option value="amazon">Amazon</Select.Option>
|
||||
<Select.Option value="ebay">eBay</Select.Option>
|
||||
<Select.Option value="shopify">Shopify</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="商品详情"
|
||||
visible={isDetailModalVisible}
|
||||
onCancel={() => setIsDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
{viewingProduct && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Text strong>商品名称:</Text>
|
||||
<div>{viewingProduct.name}</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>SKU:</Text>
|
||||
<div>{viewingProduct.sku}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||
<Col span={8}>
|
||||
<Text strong>价格:</Text>
|
||||
<div>¥{viewingProduct.price.toFixed(2)}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Text strong>成本:</Text>
|
||||
<div>¥{viewingProduct.cost.toFixed(2)}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Text strong>利润:</Text>
|
||||
<div>¥{(viewingProduct.price - viewingProduct.cost).toFixed(2)}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||
<Col span={8}>
|
||||
<Text strong>库存:</Text>
|
||||
<div>{viewingProduct.stock}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Text strong>状态:</Text>
|
||||
<div>
|
||||
<Tag color={viewingProduct.status === 'published' ? 'green' : 'default'}>
|
||||
{viewingProduct.status === 'published' ? '已发布' : viewingProduct.status === 'synced' ? '已同步' : '草稿'}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Text strong>平台:</Text>
|
||||
<div>{viewingProduct.platform}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="商品定价"
|
||||
visible={isPricingModalVisible}
|
||||
onOk={handlePricingModalOk}
|
||||
onCancel={() => setIsPricingModalVisible(false)}
|
||||
width={600}
|
||||
>
|
||||
<Form form={pricingForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="cost"
|
||||
label="成本"
|
||||
rules={[{ required: true, message: '请输入成本' }]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入成本"
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={2}
|
||||
onChange={handlePricingChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="价格"
|
||||
rules={[{ required: true, message: '请输入价格' }]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入价格"
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
precision={2}
|
||||
onChange={handlePricingChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="profit"
|
||||
label="利润"
|
||||
>
|
||||
<InputNumber
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
precision={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="profitMargin"
|
||||
label="利润率(%)"
|
||||
>
|
||||
<InputNumber
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
precision={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductManagement;
|
||||
export default ProductManagement;
|
||||
|
||||
Reference in New Issue
Block a user