Files
makemd/dashboard/src/pages/Product/CrossPlatformManage.tsx

808 lines
27 KiB
TypeScript
Raw Normal View History

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 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 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>{info.shopInfo.region}</Tag>
{!info.shopInfo.apiSupported && <Tag 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>{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;