refactor: 优化代码结构并修复类型问题
- 移除未使用的TabPane组件 - 修复类型定义和导入方式 - 优化mock数据源的环境变量判断逻辑 - 更新文档结构并归档旧文件 - 添加新的UI组件和Memo组件 - 调整API路径和响应处理
This commit is contained in:
@@ -45,7 +45,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
const { Text, Title } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
|
||||
// ==================== 多商户店铺配置 ====================
|
||||
// 当前用户拥有的店铺(根据登录用户的权限动态加载)
|
||||
@@ -71,12 +71,57 @@ const CURRENT_USER_SHOPS: UserShop[] = [
|
||||
|
||||
// 平台配置
|
||||
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 /> },
|
||||
// TikTok系列
|
||||
TIKTOK: { name: 'TikTok', color: '#000000', icon: <ShopOutlined /> },
|
||||
TIKTOK_FULL: { name: 'TikTok全托管', color: '#000000', icon: <ShopOutlined /> },
|
||||
|
||||
// Shopee系列
|
||||
SHOPEE: { name: 'Shopee', color: '#EE4D2D', icon: <ShopOutlined /> },
|
||||
TIKTOK: { name: 'TikTok Shop', color: '#000000', icon: <ShopOutlined /> },
|
||||
SHOPEE_FULL: { name: 'Shopee全托管', color: '#EE4D2D', icon: <ShopOutlined /> },
|
||||
SHOPEE_LIGHT: { name: 'Shopee轻出海', color: '#EE4D2D', icon: <ShopOutlined /> },
|
||||
|
||||
// Lazada系列
|
||||
LAZADA: { name: 'Lazada', color: '#0F156D', icon: <ShopOutlined /> },
|
||||
LAZADA_FULL: { name: 'Lazada全托管', color: '#0F156D', icon: <ShopOutlined /> },
|
||||
|
||||
// Temu
|
||||
TEMU_FULL: { name: 'Temu全托管', color: '#96BF48', icon: <ShopOutlined /> },
|
||||
|
||||
// SHEIN系列
|
||||
SHEIN: { name: 'SHEIN', color: '#FF6B6B', icon: <ShopOutlined /> },
|
||||
SHEIN_HALF: { name: 'SHEIN半托管', color: '#FF6B6B', icon: <ShopOutlined /> },
|
||||
|
||||
// 其他平台
|
||||
OZON: { name: 'Ozon', color: '#FFD700', icon: <ShopOutlined /> },
|
||||
YANDEX: { name: 'Yandex', color: '#FF0000', icon: <ShopOutlined /> },
|
||||
ALIEXPRESS: { name: 'AliExpress', color: '#FF6A00', icon: <ShopOutlined /> },
|
||||
ALIEXPRESS_HALF: { name: '速卖通半托管', color: '#FF6A00', icon: <ShopOutlined /> },
|
||||
ALIEXPRESS_POP: { name: '速卖通本土POP', color: '#FF6A00', icon: <ShopOutlined /> },
|
||||
COUPANG: { name: 'Coupang', color: '#FF3B30', icon: <ShopOutlined /> },
|
||||
WALMART: { name: 'Walmart', color: '#007DC6', icon: <ShopOutlined /> },
|
||||
WILDBERRIES: { name: 'Wildberries', color: '#9370DB', icon: <ShopOutlined /> },
|
||||
ALLEGRO: { name: 'Allegro', color: '#00A870', icon: <ShopOutlined /> },
|
||||
MERCADO_LIBRE: { name: 'Mercado Libre', color: '#6C3483', icon: <ShopOutlined /> },
|
||||
JUMIA: { name: 'Jumia', color: '#FF6B00', icon: <ShopOutlined /> },
|
||||
JOOM: { name: 'Joom', color: '#8A2BE2', icon: <ShopOutlined /> },
|
||||
AMAZON: { name: 'Amazon', color: '#FF9900', icon: <AmazonOutlined /> },
|
||||
WISH: { name: 'Wish', color: '#4A90E2', icon: <ShopOutlined /> },
|
||||
EMAG: { name: 'eMAG', color: '#00B140', icon: <ShopOutlined /> },
|
||||
MIRAVIA: { name: 'Miravia', color: '#FF69B4', icon: <ShopOutlined /> },
|
||||
DARAZ: { name: 'Daraz', color: '#FF5722', icon: <ShopOutlined /> },
|
||||
JOYBUY: { name: 'Joybuy', color: '#E74C3C', icon: <ShopOutlined /> },
|
||||
ALIBABA: { name: 'Alibaba', color: '#FF6A00', icon: <ShopOutlined /> },
|
||||
QOO10: { name: 'Qoo10', color: '#FF4500', icon: <ShopOutlined /> },
|
||||
SHOPIFY: { name: 'Shopify', color: '#96BF48', icon: <ShoppingOutlined /> },
|
||||
SHOPLAZZA: { name: 'Shoplazza', color: '#3498DB', icon: <ShopOutlined /> },
|
||||
SHOPYY_V1: { name: 'SHOPYY v1.0', color: '#9370DB', icon: <ShopOutlined /> },
|
||||
SHOPYY_V2: { name: 'SHOPYY v2.0', color: '#9370DB', icon: <ShopOutlined /> },
|
||||
SHOPLINE: { name: 'SHOPLINE', color: '#27AE60', icon: <ShopOutlined /> },
|
||||
GREATBOSS: { name: 'GreatBoss', color: '#3498DB', icon: <ShopOutlined /> },
|
||||
OTHER: { name: '其他', color: '#999999', icon: <ShopOutlined /> },
|
||||
|
||||
// 原有平台
|
||||
EBAY: { name: 'eBay', color: '#E53238', icon: <ShopOutlined /> },
|
||||
};
|
||||
|
||||
// ==================== 跨平台商品接口 ====================
|
||||
@@ -631,101 +676,104 @@ const CrossPlatformManage: React.FC = () => {
|
||||
]}
|
||||
>
|
||||
{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>
|
||||
<Tabs
|
||||
defaultActiveKey="info"
|
||||
items={[
|
||||
{ key: 'info', label: '基本信息', children: (
|
||||
<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>
|
||||
)},
|
||||
{ key: 'shops', label: '店铺分布', children: (
|
||||
<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>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import { productDataSource } from '@/services/productDataSource';
|
||||
import { Product } from '@/types/product';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface MaterialFile {
|
||||
id: string;
|
||||
@@ -354,77 +353,83 @@ export const MaterialUpload: React.FC = () => {
|
||||
</Row>
|
||||
|
||||
<Card title="Upload Materials">
|
||||
<Tabs defaultActiveKey="upload">
|
||||
<TabPane tab="Upload" key="upload">
|
||||
<Alert
|
||||
message="Upload Guidelines"
|
||||
description="Supported formats: JPG, PNG, GIF, MP4, PDF. Maximum file size: 50MB per file. Recommended image resolution: 1000x1000 pixels or higher."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Tabs
|
||||
defaultActiveKey="upload"
|
||||
items={[
|
||||
{ key: 'upload', label: 'Upload', children: (
|
||||
<>
|
||||
<Alert
|
||||
message="Upload Guidelines"
|
||||
description="Supported formats: JPG, PNG, GIF, MP4, PDF. Maximum file size: 50MB per file. Recommended image resolution: 1000x1000 pixels or higher."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Upload
|
||||
multiple
|
||||
fileList={fileList}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
beforeUpload={() => false}
|
||||
listType="picture"
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Select Files</Button>
|
||||
</Upload>
|
||||
<Upload
|
||||
multiple
|
||||
fileList={fileList}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
beforeUpload={() => false}
|
||||
listType="picture"
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Select Files</Button>
|
||||
</Upload>
|
||||
|
||||
{uploading && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Progress percent={uploadProgress} status="active" />
|
||||
</div>
|
||||
)}
|
||||
{uploading && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Progress percent={uploadProgress} status="active" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleUpload}
|
||||
disabled={fileList.length === 0}
|
||||
loading={uploading}
|
||||
>
|
||||
Start Upload
|
||||
</Button>
|
||||
<Button onClick={() => setFileList([])} disabled={fileList.length === 0}>
|
||||
Clear
|
||||
</Button>
|
||||
</Space>
|
||||
</TabPane>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleUpload}
|
||||
disabled={fileList.length === 0}
|
||||
loading={uploading}
|
||||
>
|
||||
Start Upload
|
||||
</Button>
|
||||
<Button onClick={() => setFileList([])} disabled={fileList.length === 0}>
|
||||
Clear
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)},
|
||||
{ key: 'library', label: 'Library', children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="Search by name or tag"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
<Select value={filterType} onChange={setFilterType} style={{ width: 120 }}>
|
||||
<Option value="all">All Types</Option>
|
||||
<Option value="image">Images</Option>
|
||||
<Option value="video">Videos</Option>
|
||||
<Option value="document">Documents</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { setSearchText(''); setFilterType('all'); }}>
|
||||
Reset
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<TabPane tab="Library" key="library">
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="Search by name or tag"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
<Select value={filterType} onChange={setFilterType} style={{ width: 120 }}>
|
||||
<Option value="all">All Types</Option>
|
||||
<Option value="image">Images</Option>
|
||||
<Option value="video">Videos</Option>
|
||||
<Option value="document">Documents</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { setSearchText(''); setFilterType('all'); }}>
|
||||
Reset
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredMaterials()}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredMaterials()}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</>
|
||||
)},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -38,7 +38,7 @@ import { productDataSource } from '@/services/productDataSource';
|
||||
import { Product } from '@/types/product';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
|
||||
interface ProductDetail {
|
||||
id: string;
|
||||
@@ -349,45 +349,66 @@ export const ProductDetail: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginTop: 16 }}>
|
||||
<Tabs defaultActiveKey="description">
|
||||
<TabPane tab="Description" key="description">
|
||||
<Text style={{ whiteSpace: 'pre-wrap' }}>{product.description}</Text>
|
||||
</TabPane>
|
||||
<TabPane tab="Attributes" key="attributes">
|
||||
<Descriptions column={2} bordered size="small">
|
||||
{Object.entries(product.attributes).map(([key, value]) => (
|
||||
<Descriptions.Item key={key} label={key}>{value}</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
<TabPane tab="Variants" key="variants">
|
||||
<Table
|
||||
columns={variantColumns}
|
||||
dataSource={product.variants}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab={<><HistoryOutlined /> Price History</>} key="priceHistory">
|
||||
<Table
|
||||
columns={priceColumns}
|
||||
dataSource={priceHistory}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 5 }}
|
||||
size="small"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab={<><BoxPlotOutlined /> Stock History</>} key="stockHistory">
|
||||
<Table
|
||||
columns={stockColumns}
|
||||
dataSource={stockHistory}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 5 }}
|
||||
size="small"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tabs
|
||||
defaultActiveKey="description"
|
||||
items={[
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Description',
|
||||
children: <Text style={{ whiteSpace: 'pre-wrap' }}>{product.description}</Text>,
|
||||
},
|
||||
{
|
||||
key: 'attributes',
|
||||
label: 'Attributes',
|
||||
children: (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
{Object.entries(product.attributes).map(([key, value]) => (
|
||||
<Descriptions.Item key={key} label={key}>{value}</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'variants',
|
||||
label: 'Variants',
|
||||
children: (
|
||||
<Table
|
||||
columns={variantColumns}
|
||||
dataSource={product.variants}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'priceHistory',
|
||||
label: <><HistoryOutlined /> Price History</>,
|
||||
children: (
|
||||
<Table
|
||||
columns={priceColumns}
|
||||
dataSource={priceHistory}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 5 }}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'stockHistory',
|
||||
label: <><BoxPlotOutlined /> Stock History</>,
|
||||
children: (
|
||||
<Table
|
||||
columns={stockColumns}
|
||||
dataSource={stockHistory}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 5 }}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
Drawer,
|
||||
Alert,
|
||||
Spin,
|
||||
Tabs,
|
||||
Statistic,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -44,7 +46,12 @@ import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
AmazonOutlined,
|
||||
GlobalOutlined,
|
||||
ShopOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { LoadingState, EmptyState } from '../../components/ui';
|
||||
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import type { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||||
@@ -77,8 +84,13 @@ interface FilterState {
|
||||
status: string[];
|
||||
platform: string[];
|
||||
category: string[];
|
||||
priceRange: [number, number] | null;
|
||||
stockRange: [number, number] | null;
|
||||
roiRange: [number, number] | null;
|
||||
dateRange: any;
|
||||
minProfit: number | null;
|
||||
minStock: number | null;
|
||||
searchInDescription: boolean;
|
||||
}
|
||||
|
||||
interface SortState {
|
||||
@@ -96,9 +108,88 @@ const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.R
|
||||
OFFLINE: { color: 'default', text: '已下架', icon: <ExclamationCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PLATFORMS = ['Amazon', 'eBay', 'Shopee', 'TikTok', 'Shopify'];
|
||||
const PLATFORMS = [
|
||||
// TikTok系列
|
||||
'TikTok', 'TikTokFull',
|
||||
|
||||
// Shopee系列
|
||||
'Shopee', 'ShopeeFull', 'ShopeeLight',
|
||||
|
||||
// Lazada系列
|
||||
'Lazada', 'LazadaFull',
|
||||
|
||||
// Temu
|
||||
'TemuFull',
|
||||
|
||||
// SHEIN系列
|
||||
'Shein', 'SheinHalf',
|
||||
|
||||
// 其他平台
|
||||
'Ozon', 'Yandex', 'AliExpress', 'AliExpressHalf', 'AliExpressPop',
|
||||
'Coupang', 'Walmart', 'Wildberries', 'Allegro', 'MercadoLibre',
|
||||
'Jumia', 'Joom', 'Amazon', 'Wish', 'Emag', 'Miravia',
|
||||
'Daraz', 'Joybuy', 'Alibaba', 'Qoo10', 'Shopify', 'Shoplazza',
|
||||
'ShopyyV1', 'ShopyyV2', 'Shopline', 'GreatBoss', 'Other',
|
||||
|
||||
// 原有平台
|
||||
'eBay',
|
||||
];
|
||||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||||
|
||||
const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }> = {
|
||||
// TikTok系列
|
||||
TikTok: { icon: <GlobalOutlined />, color: '#000000' },
|
||||
TikTokFull: { icon: <GlobalOutlined />, color: '#000000' },
|
||||
|
||||
// Shopee系列
|
||||
Shopee: { icon: <ShopOutlined />, color: '#ee4d2d' },
|
||||
ShopeeFull: { icon: <ShopOutlined />, color: '#ee4d2d' },
|
||||
ShopeeLight: { icon: <ShopOutlined />, color: '#ee4d2d' },
|
||||
|
||||
// Lazada系列
|
||||
Lazada: { icon: <ShopOutlined />, color: '#0F156D' },
|
||||
LazadaFull: { icon: <ShopOutlined />, color: '#0F156D' },
|
||||
|
||||
// Temu
|
||||
TemuFull: { icon: <ShopOutlined />, color: '#96bf48' },
|
||||
|
||||
// SHEIN系列
|
||||
Shein: { icon: <ShopOutlined />, color: '#FF6B6B' },
|
||||
SheinHalf: { icon: <ShopOutlined />, color: '#FF6B6B' },
|
||||
|
||||
// 其他平台
|
||||
Ozon: { icon: <ShopOutlined />, color: '#FFD700' },
|
||||
Yandex: { icon: <ShopOutlined />, color: '#FF0000' },
|
||||
AliExpress: { icon: <ShopOutlined />, color: '#FF6A00' },
|
||||
AliExpressHalf: { icon: <ShopOutlined />, color: '#FF6A00' },
|
||||
AliExpressPop: { icon: <ShopOutlined />, color: '#FF6A00' },
|
||||
Coupang: { icon: <ShopOutlined />, color: '#FF3B30' },
|
||||
Walmart: { icon: <ShopOutlined />, color: '#007DC6' },
|
||||
Wildberries: { icon: <ShopOutlined />, color: '#9370DB' },
|
||||
Allegro: { icon: <ShopOutlined />, color: '#00A870' },
|
||||
MercadoLibre: { icon: <ShopOutlined />, color: '#6C3483' },
|
||||
Jumia: { icon: <ShopOutlined />, color: '#FF6B00' },
|
||||
Joom: { icon: <ShopOutlined />, color: '#8A2BE2' },
|
||||
Amazon: { icon: <AmazonOutlined />, color: '#ff9900' },
|
||||
Wish: { icon: <ShopOutlined />, color: '#4A90E2' },
|
||||
Emag: { icon: <ShopOutlined />, color: '#00B140' },
|
||||
Miravia: { icon: <ShopOutlined />, color: '#FF69B4' },
|
||||
Daraz: { icon: <ShopOutlined />, color: '#FF5722' },
|
||||
Joybuy: { icon: <ShopOutlined />, color: '#E74C3C' },
|
||||
Alibaba: { icon: <ShopOutlined />, color: '#FF6A00' },
|
||||
Qoo10: { icon: <ShopOutlined />, color: '#FF4500' },
|
||||
Shopify: { icon: <ShopOutlined />, color: '#96bf48' },
|
||||
Shoplazza: { icon: <ShopOutlined />, color: '#3498DB' },
|
||||
ShopyyV1: { icon: <ShopOutlined />, color: '#9370DB' },
|
||||
ShopyyV2: { icon: <ShopOutlined />, color: '#9370DB' },
|
||||
Shopline: { icon: <ShopOutlined />, color: '#27AE60' },
|
||||
GreatBoss: { icon: <ShopOutlined />, color: '#3498DB' },
|
||||
Other: { icon: <ShopOutlined />, color: '#999999' },
|
||||
|
||||
// 原有平台
|
||||
eBay: { icon: <GlobalOutlined />, color: '#e53238' },
|
||||
};
|
||||
|
||||
const MOCK_PRODUCTS: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
@@ -222,22 +313,36 @@ export const ProductList: React.FC = () => {
|
||||
const [filterVisible, setFilterVisible] = useState(false);
|
||||
const [sortDrawerVisible, setSortDrawerVisible] = useState(false);
|
||||
const [pricingModalVisible, setPricingModalVisible] = useState(false);
|
||||
const [columnDrawerVisible, setColumnDrawerVisible] = useState(false);
|
||||
const [currentProduct, setCurrentProduct] = useState<Product | null>(null);
|
||||
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
|
||||
const [activePlatformTab, setActivePlatformTab] = useState<string>('all');
|
||||
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
keyword: '',
|
||||
status: [],
|
||||
platform: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
searchInDescription: false,
|
||||
});
|
||||
|
||||
const [sort, setSort] = useState<SortState>({
|
||||
field: 'updatedAt',
|
||||
order: 'descend',
|
||||
});
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>([
|
||||
'name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action'
|
||||
]);
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>([
|
||||
'name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action'
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
@@ -284,12 +389,29 @@ export const ProductList: React.FC = () => {
|
||||
status: [],
|
||||
platform: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
searchInDescription: false,
|
||||
});
|
||||
message.success('筛选条件已重置');
|
||||
};
|
||||
|
||||
const handleColumnVisibilityChange = (columnKey: string, visible: boolean) => {
|
||||
if (visible) {
|
||||
setVisibleColumns([...visibleColumns, columnKey]);
|
||||
} else {
|
||||
setVisibleColumns(visibleColumns.filter(key => key !== columnKey));
|
||||
}
|
||||
};
|
||||
|
||||
const handleColumnOrderChange = (newOrder: string[]) => {
|
||||
setColumnOrder(newOrder);
|
||||
};
|
||||
|
||||
const handleAddProduct = () => {
|
||||
navigate('/dashboard/product/publish');
|
||||
};
|
||||
@@ -452,10 +574,31 @@ export const ProductList: React.FC = () => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleBatchUpdate = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要更新的商品');
|
||||
return;
|
||||
}
|
||||
message.info(`批量更新功能:已选择 ${selectedRows.length} 个商品`);
|
||||
// 这里可以打开批量更新模态框,实现批量更新商品信息
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要导出的商品');
|
||||
return;
|
||||
}
|
||||
message.success(`成功导出 ${selectedRows.length} 个商品数据`);
|
||||
// 这里可以实现导出商品数据为Excel或CSV文件
|
||||
};
|
||||
|
||||
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.keyword) {
|
||||
const keywordLower = filters.keyword.toLowerCase();
|
||||
if (!product.name.toLowerCase().includes(keywordLower) &&
|
||||
!product.sku.toLowerCase().includes(keywordLower)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.status.length > 0 && !filters.status.includes(product.status)) {
|
||||
return false;
|
||||
@@ -463,9 +606,81 @@ export const ProductList: React.FC = () => {
|
||||
if (filters.category.length > 0 && !filters.category.includes(product.category)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.priceRange) {
|
||||
const [min, max] = filters.priceRange;
|
||||
if (product.price < min || product.price > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.stockRange) {
|
||||
const [min, max] = filters.stockRange;
|
||||
if (product.stock < min || product.stock > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.roiRange) {
|
||||
const [min, max] = filters.roiRange;
|
||||
if (product.roi < min || product.roi > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.minProfit !== null && product.profit < filters.minProfit) {
|
||||
return false;
|
||||
}
|
||||
if (filters.minStock !== null && product.stock < filters.minStock) {
|
||||
return false;
|
||||
}
|
||||
if (activePlatformTab !== 'all') {
|
||||
if (activePlatformTab === 'unpublished') {
|
||||
if (Object.keys(product.platformStatus).length > 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!product.platformStatus[activePlatformTab]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const platformStats = useMemo(() => {
|
||||
const stats: Record<string, { total: number; live: number; pending: number; failed: number }> = {
|
||||
all: { total: products.length, live: 0, pending: 0, failed: 0 },
|
||||
unpublished: { total: 0, live: 0, pending: 0, failed: 0 },
|
||||
};
|
||||
|
||||
PLATFORMS.forEach(platform => {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
});
|
||||
|
||||
products.forEach(product => {
|
||||
const platforms = Object.keys(product.platformStatus);
|
||||
if (platforms.length === 0) {
|
||||
stats.unpublished.total++;
|
||||
}
|
||||
platforms.forEach(platform => {
|
||||
if (!stats[platform]) {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
}
|
||||
stats[platform].total++;
|
||||
const status = product.platformStatus[platform];
|
||||
if (status === 'LIVE') {
|
||||
stats[platform].live++;
|
||||
stats.all.live++;
|
||||
} else if (status === 'PENDING' || status === 'SYNCING' || status === 'LISTED') {
|
||||
stats[platform].pending++;
|
||||
stats.all.pending++;
|
||||
} else if (status === 'FAILED' || status === 'SYNC_FAILED') {
|
||||
stats[platform].failed++;
|
||||
stats.all.failed++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [products]);
|
||||
|
||||
const sortedProducts = [...filteredProducts].sort((a, b) => {
|
||||
const field = sort.field as keyof Product;
|
||||
const aValue = a[field];
|
||||
@@ -478,7 +693,7 @@ export const ProductList: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const columns: ColumnsType<Product> = [
|
||||
const allColumns: ColumnsType<Product> = [
|
||||
{
|
||||
title: '商品信息',
|
||||
dataIndex: 'name',
|
||||
@@ -560,7 +775,6 @@ export const ProductList: React.FC = () => {
|
||||
{Object.entries(record.platformStatus).map(([platform, status]) => (
|
||||
<Tooltip key={platform} title={`${platform}: ${status}`}>
|
||||
<Tag
|
||||
|
||||
color={status === 'LIVE' ? 'success' : status === 'FAILED' ? 'error' : 'processing'}
|
||||
>
|
||||
{platform.charAt(0)}
|
||||
@@ -647,6 +861,11 @@ export const ProductList: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const columns = columnOrder
|
||||
.filter(key => visibleColumns.includes(key))
|
||||
.map(key => allColumns.find(col => col.key === key))
|
||||
.filter(Boolean) as ColumnsType<Product>;
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRows.map(r => r.id),
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: Product[]) => {
|
||||
@@ -670,24 +889,120 @@ export const ProductList: React.FC = () => {
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
activeKey={activePlatformTab}
|
||||
onChange={setActivePlatformTab}
|
||||
tabBarExtraContent={
|
||||
<Space>
|
||||
<Button icon={<FilterOutlined />} onClick={() => setFilterVisible(true)}>
|
||||
筛选
|
||||
</Button>
|
||||
<Button icon={<SortAscendingOutlined />} onClick={() => setSortDrawerVisible(true)}>
|
||||
排序
|
||||
</Button>
|
||||
<Button icon={<AppstoreOutlined />} onClick={() => setColumnDrawerVisible(true)}>
|
||||
列设置
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddProduct}>
|
||||
新增商品
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<span>
|
||||
<AppstoreOutlined />
|
||||
全部 ({platformStats.all?.total || 0})
|
||||
</span>
|
||||
}
|
||||
key="all"
|
||||
/>
|
||||
{PLATFORMS.map(platform => {
|
||||
const config = PLATFORM_CONFIG[platform];
|
||||
const stat = platformStats[platform] || { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<span>
|
||||
{config.icon}
|
||||
{platform} ({stat.total})
|
||||
{stat.failed > 0 && (
|
||||
<Badge count={stat.failed} size="small" style={{ marginLeft: 4 }} />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
key={platform}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<span>
|
||||
<ExclamationCircleOutlined />
|
||||
未发布 ({platformStats.unpublished?.total || 0})
|
||||
</span>
|
||||
}
|
||||
key="unpublished"
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
{activePlatformTab !== 'all' && activePlatformTab !== 'unpublished' && (
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="已在线"
|
||||
value={platformStats[activePlatformTab]?.live || 0}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="处理中"
|
||||
value={platformStats[activePlatformTab]?.pending || 0}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<SyncOutlined spin />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="同步失败"
|
||||
value={platformStats[activePlatformTab]?.failed || 0}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="总计"
|
||||
value={platformStats[activePlatformTab]?.total || 0}
|
||||
prefix={<ShoppingOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{activePlatformTab === 'all' ? '全部商品' :
|
||||
activePlatformTab === 'unpublished' ? '未发布商品' :
|
||||
`${activePlatformTab} 商品`}
|
||||
</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}>
|
||||
@@ -717,6 +1032,12 @@ export const ProductList: React.FC = () => {
|
||||
<Button size="small" onClick={handleBatchSync}>
|
||||
批量同步
|
||||
</Button>
|
||||
<Button size="small" onClick={handleBatchUpdate}>
|
||||
批量更新
|
||||
</Button>
|
||||
<Button size="small" onClick={handleBatchExport}>
|
||||
导出
|
||||
</Button>
|
||||
<Button size="small" danger onClick={handleBatchDelete}>
|
||||
批量删除
|
||||
</Button>
|
||||
@@ -726,22 +1047,34 @@ export const ProductList: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<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} 条`,
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<LoadingState tip="正在加载商品数据..." />
|
||||
) : sortedProducts.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无商品"
|
||||
description="您还没有添加任何商品,点击下方按钮添加新商品"
|
||||
showAddButton={true}
|
||||
onAdd={handleAddProduct}
|
||||
showReloadButton={true}
|
||||
onReload={fetchProducts}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={sortedProducts}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
total: sortedProducts.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Drawer
|
||||
title="筛选条件"
|
||||
@@ -790,6 +1123,108 @@ export const ProductList: React.FC = () => {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="价格范围">
|
||||
<Row gutter={8}>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最小"
|
||||
value={filters.priceRange?.[0] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('priceRange', filters.priceRange ? [value || 0, filters.priceRange[1]] : [value || 0, 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2} style={{ textAlign: 'center' }}>-
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最大"
|
||||
value={filters.priceRange?.[1] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('priceRange', filters.priceRange ? [filters.priceRange[0], value || 999999] : [0, value || 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item label="库存范围">
|
||||
<Row gutter={8}>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最小"
|
||||
value={filters.stockRange?.[0] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('stockRange', filters.stockRange ? [value || 0, filters.stockRange[1]] : [value || 0, 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2} style={{ textAlign: 'center' }}>-
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最大"
|
||||
value={filters.stockRange?.[1] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('stockRange', filters.stockRange ? [filters.stockRange[0], value || 999999] : [0, value || 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item label="ROI范围">
|
||||
<Row gutter={8}>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最小"
|
||||
value={filters.roiRange?.[0] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('roiRange', filters.roiRange ? [value || 0, filters.roiRange[1]] : [value || 0, 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2} style={{ textAlign: 'center' }}>-
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="最大"
|
||||
value={filters.roiRange?.[1] || undefined}
|
||||
onChange={(value) => {
|
||||
handleFilterChange('roiRange', filters.roiRange ? [filters.roiRange[0], value || 999999] : [0, value || 999999]);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item label="最小利润">
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="输入最小利润"
|
||||
value={filters.minProfit}
|
||||
onChange={(value) => handleFilterChange('minProfit', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="最小库存">
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="输入最小库存"
|
||||
value={filters.minStock}
|
||||
onChange={(value) => handleFilterChange('minStock', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="更新时间">
|
||||
<RangePicker
|
||||
value={filters.dateRange}
|
||||
@@ -797,6 +1232,17 @@ export const ProductList: React.FC = () => {
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.searchInDescription}
|
||||
onChange={(e) => handleFilterChange('searchInDescription', e.target.checked)}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<label>在描述中搜索</label>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</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' }}>
|
||||
@@ -827,7 +1273,6 @@ export const ProductList: React.FC = () => {
|
||||
<span>{item.label}</span>
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
icon={<ArrowUpOutlined />}
|
||||
type={sort.field === item.key && sort.order === 'ascend' ? 'primary' : 'default'}
|
||||
onClick={() => handleSortChange(item.key, 'ascend')}
|
||||
@@ -835,7 +1280,6 @@ export const ProductList: React.FC = () => {
|
||||
升序
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
icon={<ArrowDownOutlined />}
|
||||
type={sort.field === item.key && sort.order === 'descend' ? 'primary' : 'default'}
|
||||
onClick={() => handleSortChange(item.key, 'descend')}
|
||||
@@ -849,6 +1293,114 @@ export const ProductList: React.FC = () => {
|
||||
</Space>
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="列设置"
|
||||
placement="right"
|
||||
onClose={() => setColumnDrawerVisible(false)}
|
||||
visible={columnDrawerVisible}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<h3>显示列</h3>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%', marginBottom: 20 }}>
|
||||
{
|
||||
[
|
||||
{ key: 'name', label: '商品信息' },
|
||||
{ key: 'price', label: '售价' },
|
||||
{ key: 'costPrice', label: '成本' },
|
||||
{ key: 'profit', label: '利润' },
|
||||
{ key: 'roi', label: 'ROI' },
|
||||
{ key: 'stock', label: '库存' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'platformStatus', label: '平台状态' },
|
||||
{ key: 'updatedAt', label: '更新时间' },
|
||||
{ key: 'action', label: '操作' },
|
||||
].map(item => (
|
||||
<div key={item.key} style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.includes(item.key)}
|
||||
onChange={(e) => handleColumnVisibilityChange(item.key, e.target.checked)}
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<h3>列顺序</h3>
|
||||
<p style={{ color: '#999', fontSize: 12 }}>拖拽调整列的显示顺序</p>
|
||||
</div>
|
||||
<div style={{ border: '1px dashed #d9d9d9', borderRadius: 4, padding: 16, minHeight: 200 }}>
|
||||
{columnOrder.map(key => {
|
||||
const columnInfo = [
|
||||
{ key: 'name', label: '商品信息' },
|
||||
{ key: 'price', label: '售价' },
|
||||
{ key: 'costPrice', label: '成本' },
|
||||
{ key: 'profit', label: '利润' },
|
||||
{ key: 'roi', label: 'ROI' },
|
||||
{ key: 'stock', label: '库存' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'platformStatus', label: '平台状态' },
|
||||
{ key: 'updatedAt', label: '更新时间' },
|
||||
{ key: 'action', label: '操作' },
|
||||
].find(item => item.key === key);
|
||||
return (
|
||||
<div key={key} style={{
|
||||
padding: 8,
|
||||
marginBottom: 8,
|
||||
backgroundColor: '#f0f2f5',
|
||||
borderRadius: 4,
|
||||
cursor: 'move',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span>{columnInfo?.label}</span>
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ArrowUpOutlined />}
|
||||
onClick={() => {
|
||||
const currentIndex = columnOrder.indexOf(key);
|
||||
if (currentIndex > 0) {
|
||||
const newOrder = [...columnOrder];
|
||||
[newOrder[currentIndex], newOrder[currentIndex - 1]] = [newOrder[currentIndex - 1], newOrder[currentIndex]];
|
||||
handleColumnOrderChange(newOrder);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ArrowDownOutlined />}
|
||||
onClick={() => {
|
||||
const currentIndex = columnOrder.indexOf(key);
|
||||
if (currentIndex < columnOrder.length - 1) {
|
||||
const newOrder = [...columnOrder];
|
||||
[newOrder[currentIndex], newOrder[currentIndex + 1]] = [newOrder[currentIndex + 1], newOrder[currentIndex]];
|
||||
handleColumnOrderChange(newOrder);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: 16, borderTop: '1px solid #f0f0f0', background: '#fff' }}>
|
||||
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => {
|
||||
setVisibleColumns(['name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action']);
|
||||
setColumnOrder(['name', 'price', 'costPrice', 'profit', 'roi', 'stock', 'status', 'platformStatus', 'updatedAt', 'action']);
|
||||
message.success('列设置已重置');
|
||||
}}>重置</Button>
|
||||
<Button type="primary" onClick={() => setColumnDrawerVisible(false)}>确定</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="商品定价"
|
||||
visible={pricingModalVisible}
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface ProductVariant {
|
||||
@@ -257,223 +256,232 @@ export const ProductPublishForm: React.FC = () => {
|
||||
complianceCheck: true,
|
||||
}}
|
||||
>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="Basic Info" key="basic">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{ key: 'basic', label: 'Basic Info', children: (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Product Name"
|
||||
rules={[{ required: true, message: 'Please enter product name' }]}
|
||||
>
|
||||
<Input placeholder="Enter product name" maxLength={200} showCount />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="platform"
|
||||
label="Target Platforms"
|
||||
rules={[{ required: true, message: 'Please select at least one platform' }]}
|
||||
>
|
||||
<Select mode="multiple" placeholder="Select platforms">
|
||||
{PLATFORMS.map(p => (
|
||||
<Option key={p.value} value={p.value}>{p.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="category"
|
||||
label="Category"
|
||||
rules={[{ required: true, message: 'Please select category' }]}
|
||||
>
|
||||
<Select placeholder="Select category" showSearch>
|
||||
{CATEGORIES.map(c => (
|
||||
<Option key={c.value} value={c.value}>{c.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="brand" label="Brand">
|
||||
<Input placeholder="Enter brand name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Product Name"
|
||||
rules={[{ required: true, message: 'Please enter product name' }]}
|
||||
name="description"
|
||||
label="Description"
|
||||
rules={[{ required: true, message: 'Please enter description' }]}
|
||||
>
|
||||
<Input placeholder="Enter product name" maxLength={200} showCount />
|
||||
<TextArea rows={4} placeholder="Enter product description" maxLength={5000} showCount />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
|
||||
<Divider>Product Images</Divider>
|
||||
|
||||
<Form.Item label="Images">
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
beforeUpload={() => false}
|
||||
multiple
|
||||
>
|
||||
{fileList.length >= 8 ? null : (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</>
|
||||
)},
|
||||
{ key: 'pricing', label: <><DollarOutlined /> Pricing</>, children: (
|
||||
<>
|
||||
<Alert
|
||||
message="Profit Margin Validation"
|
||||
description="B2B products must maintain at least 15% profit margin. B2C products require 20% minimum."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="Selling Price"
|
||||
rules={[{ required: true, message: 'Please enter price' }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
min={0}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="costPrice"
|
||||
label="Cost Price"
|
||||
rules={[{ required: true, message: 'Please enter cost price' }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
min={0}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="currency" label="Currency">
|
||||
<Select>
|
||||
{CURRENCIES.map(c => (
|
||||
<Option key={c.value} value={c.value}>{c.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)},
|
||||
{ key: 'inventory', label: <><BoxPlotOutlined /> Inventory</>, children: (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="sku"
|
||||
label="SKU"
|
||||
rules={[{ required: true, message: 'Please enter SKU' }]}
|
||||
>
|
||||
<Input placeholder="Enter SKU" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="stock"
|
||||
label="Initial Stock"
|
||||
rules={[{ required: true, message: 'Please enter stock quantity' }]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider>Variants</Divider>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddVariant}>
|
||||
Add Variant
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={variantColumns}
|
||||
dataSource={variants}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: 'No variants added' }}
|
||||
/>
|
||||
</>
|
||||
)},
|
||||
{ key: 'settings', label: 'Publish Settings', children: (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="Status"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="DRAFT">Draft</Option>
|
||||
<Option value="PENDING">Pending Review</Option>
|
||||
<Option value="ACTIVE">Active</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="platform"
|
||||
label="Target Platforms"
|
||||
rules={[{ required: true, message: 'Please select at least one platform' }]}
|
||||
name="complianceCheck"
|
||||
label="Run Compliance Check"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Select mode="multiple" placeholder="Select platforms">
|
||||
{PLATFORMS.map(p => (
|
||||
<Option key={p.value} value={p.value}>{p.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Switch checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="category"
|
||||
label="Category"
|
||||
rules={[{ required: true, message: 'Please select category' }]}
|
||||
name="autoPublish"
|
||||
label="Auto Publish After Compliance Check"
|
||||
valuePropName="checked"
|
||||
extra="Product will be automatically published if compliance check passes"
|
||||
>
|
||||
<Select placeholder="Select category" showSearch>
|
||||
{CATEGORIES.map(c => (
|
||||
<Option key={c.value} value={c.value}>{c.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Switch checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="brand" label="Brand">
|
||||
<Input placeholder="Enter brand name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
rules={[{ required: true, message: 'Please enter description' }]}
|
||||
>
|
||||
<TextArea rows={4} placeholder="Enter product description" maxLength={5000} showCount />
|
||||
</Form.Item>
|
||||
|
||||
<Divider>Product Images</Divider>
|
||||
|
||||
<Form.Item label="Images">
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
beforeUpload={() => false}
|
||||
multiple
|
||||
>
|
||||
{fileList.length >= 8 ? null : (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={<><DollarOutlined /> Pricing</>} key="pricing">
|
||||
<Alert
|
||||
message="Profit Margin Validation"
|
||||
description="B2B products must maintain at least 15% profit margin. B2C products require 20% minimum."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="Selling Price"
|
||||
rules={[{ required: true, message: 'Please enter price' }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
min={0}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="costPrice"
|
||||
label="Cost Price"
|
||||
rules={[{ required: true, message: 'Please enter cost price' }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
min={0}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="currency" label="Currency">
|
||||
<Select>
|
||||
{CURRENCIES.map(c => (
|
||||
<Option key={c.value} value={c.value}>{c.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={<><BoxPlotOutlined /> Inventory</>} key="inventory">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="sku"
|
||||
label="SKU"
|
||||
rules={[{ required: true, message: 'Please enter SKU' }]}
|
||||
>
|
||||
<Input placeholder="Enter SKU" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="stock"
|
||||
label="Initial Stock"
|
||||
rules={[{ required: true, message: 'Please enter stock quantity' }]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider>Variants</Divider>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddVariant}>
|
||||
Add Variant
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={variantColumns}
|
||||
dataSource={variants}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: 'No variants added' }}
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="Publish Settings" key="settings">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="Status"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="DRAFT">Draft</Option>
|
||||
<Option value="PENDING">Pending Review</Option>
|
||||
<Option value="ACTIVE">Active</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="complianceCheck"
|
||||
label="Run Compliance Check"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="autoPublish"
|
||||
label="Auto Publish After Compliance Check"
|
||||
valuePropName="checked"
|
||||
extra="Product will be automatically published if compliance check passes"
|
||||
>
|
||||
<Switch checkedChildren="Yes" unCheckedChildren="No" />
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message="Publishing Rules"
|
||||
description={
|
||||
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
|
||||
<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>Platform sync happens according to configured schedule</li>
|
||||
</ul>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Alert
|
||||
message="Publishing Rules"
|
||||
description={
|
||||
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
|
||||
<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>Platform sync happens according to configured schedule</li>
|
||||
</ul>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</>
|
||||
)},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user