feat: 新增多模块功能与服务实现

新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
This commit is contained in:
2026-03-18 09:38:09 +08:00
parent 72cd7f6f45
commit 037e412aad
158 changed files with 50217 additions and 1313 deletions

View File

@@ -0,0 +1,456 @@
import React, { useState } from 'react';
import {
Card,
Upload,
Button,
message,
Table,
Tag,
Space,
Modal,
Image,
Progress,
Divider,
Row,
Col,
Input,
Select,
Tooltip,
Alert,
Tabs,
} from 'antd';
import {
UploadOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
FolderOutlined,
FileImageOutlined,
FileOutlined,
VideoCameraOutlined,
ReloadOutlined,
SearchOutlined,
FilterOutlined,
} from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
const { Option } = Select;
const { TabPane } = Tabs;
interface MaterialFile {
id: string;
name: string;
type: 'image' | 'video' | 'document';
size: number;
url: string;
thumbnail?: string;
uploadedAt: string;
uploadedBy: string;
status: 'READY' | 'PROCESSING' | 'FAILED';
tags: string[];
productId?: string;
productName?: string;
}
const MOCK_MATERIALS: MaterialFile[] = [
{
id: '1',
name: 'product_main_001.jpg',
type: 'image',
size: 2456789,
url: 'https://via.placeholder.com/800x800?text=Product+1',
thumbnail: 'https://via.placeholder.com/100x100?text=1',
uploadedAt: '2026-03-18 10:30:00',
uploadedBy: 'Admin',
status: 'READY',
tags: ['main', 'product'],
productId: 'P-2026-001',
productName: 'Industrial Sensor A',
},
{
id: '2',
name: 'product_gallery_002.jpg',
type: 'image',
size: 1890234,
url: 'https://via.placeholder.com/800x800?text=Product+2',
thumbnail: 'https://via.placeholder.com/100x100?text=2',
uploadedAt: '2026-03-18 10:35:00',
uploadedBy: 'Admin',
status: 'READY',
tags: ['gallery', 'product'],
productId: 'P-2026-001',
productName: 'Industrial Sensor A',
},
{
id: '3',
name: 'product_video_demo.mp4',
type: 'video',
size: 15678901,
url: 'https://example.com/video.mp4',
thumbnail: 'https://via.placeholder.com/100x100?text=Video',
uploadedAt: '2026-03-17 14:00:00',
uploadedBy: 'Operator',
status: 'PROCESSING',
tags: ['demo', 'video'],
},
{
id: '4',
name: 'product_manual.pdf',
type: 'document',
size: 567890,
url: 'https://example.com/manual.pdf',
uploadedAt: '2026-03-16 09:00:00',
uploadedBy: 'Admin',
status: 'READY',
tags: ['manual', 'document'],
productId: 'P-2026-002',
productName: 'Control Module B',
},
];
const TYPE_CONFIG: Record<string, { color: string; icon: React.ReactNode }> = {
image: { color: 'blue', icon: <FileImageOutlined /> },
video: { color: 'purple', icon: <VideoCameraOutlined /> },
document: { color: 'green', icon: <FileOutlined /> },
};
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
READY: { color: 'success', text: 'Ready' },
PROCESSING: { color: 'processing', text: 'Processing' },
FAILED: { color: 'error', text: 'Failed' },
};
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
};
export const MaterialUpload: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [materials, setMaterials] = useState<MaterialFile[]>(MOCK_MATERIALS);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewFile, setPreviewFile] = useState<MaterialFile | null>(null);
const [searchText, setSearchText] = useState('');
const [filterType, setFilterType] = useState<string>('all');
const handleUpload = async () => {
if (fileList.length === 0) {
message.warning('Please select files to upload');
return;
}
setUploading(true);
setUploadProgress(0);
for (let i = 0; i <= 100; i += 10) {
await new Promise(resolve => setTimeout(resolve, 200));
setUploadProgress(i);
}
const newMaterials: MaterialFile[] = fileList.map((file, index) => ({
id: `${Date.now()}-${index}`,
name: file.name,
type: file.type?.startsWith('image') ? 'image' :
file.type?.startsWith('video') ? 'video' : 'document',
size: file.size || 0,
url: URL.createObjectURL(file as any),
thumbnail: file.type?.startsWith('image') ? URL.createObjectURL(file as any) : undefined,
uploadedAt: new Date().toISOString(),
uploadedBy: 'Current User',
status: 'READY',
tags: [],
}));
setMaterials([...newMaterials, ...materials]);
setFileList([]);
setUploading(false);
message.success(`${fileList.length} file(s) uploaded successfully`);
};
const handleDelete = (id: string) => {
Modal.confirm({
title: 'Delete Material',
content: 'Are you sure you want to delete this file?',
okText: 'Delete',
okType: 'danger',
onOk: () => {
setMaterials(materials.filter(m => m.id !== id));
message.success('File deleted');
},
});
};
const handlePreview = (file: MaterialFile) => {
setPreviewFile(file);
setPreviewVisible(true);
};
const handleCopyUrl = (url: string) => {
navigator.clipboard.writeText(url);
message.success('URL copied to clipboard');
};
const getFilteredMaterials = () => {
return materials.filter(m => {
const matchesSearch = m.name.toLowerCase().includes(searchText.toLowerCase()) ||
m.tags.some(t => t.toLowerCase().includes(searchText.toLowerCase()));
const matchesType = filterType === 'all' || m.type === filterType;
return matchesSearch && matchesType;
});
};
const columns = [
{
title: 'Preview',
dataIndex: 'thumbnail',
key: 'thumbnail',
width: 80,
render: (_: any, record: MaterialFile) => (
record.type === 'image' ? (
<Image
src={record.thumbnail}
alt={record.name}
width={50}
height={50}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<div style={{ width: 50, height: 50, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f5f5f5', borderRadius: 4 }}>
{TYPE_CONFIG[record.type].icon}
</div>
)
),
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name: string, record: MaterialFile) => (
<Space>
<Tag color={TYPE_CONFIG[record.type].color} icon={TYPE_CONFIG[record.type].icon}>
{record.type.toUpperCase()}
</Tag>
<span>{name}</span>
</Space>
),
},
{
title: 'Size',
dataIndex: 'size',
key: 'size',
width: 100,
render: (size: number) => formatFileSize(size),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => (
<Tag color={STATUS_CONFIG[status].color}>{STATUS_CONFIG[status].text}</Tag>
),
},
{
title: 'Product',
dataIndex: 'productName',
key: 'productName',
width: 150,
render: (name: string) => name || <Tag>Unassigned</Tag>,
},
{
title: 'Uploaded',
dataIndex: 'uploadedAt',
key: 'uploadedAt',
width: 150,
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
width: 150,
render: (tags: string[]) => tags.map(t => <Tag key={t}>{t}</Tag>),
},
{
title: 'Actions',
key: 'actions',
width: 120,
render: (_: any, record: MaterialFile) => (
<Space>
<Tooltip title="Preview">
<Button type="text" icon={<EyeOutlined />} onClick={() => handlePreview(record)} />
</Tooltip>
<Tooltip title="Copy URL">
<Button type="text" icon={<CopyOutlined />} onClick={() => handleCopyUrl(record.url)} />
</Tooltip>
<Tooltip title="Delete">
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
</Tooltip>
</Space>
),
},
];
const stats = {
total: materials.length,
images: materials.filter(m => m.type === 'image').length,
videos: materials.filter(m => m.type === 'video').length,
documents: materials.filter(m => m.type === 'document').length,
totalSize: materials.reduce((acc, m) => acc + m.size, 0),
};
return (
<div className="material-upload-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<FolderOutlined style={{ fontSize: 32, color: '#1890ff' }} />
<div>
<div style={{ color: '#666' }}>Total Files</div>
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{stats.total}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<FileImageOutlined style={{ fontSize: 32, color: '#52c41a' }} />
<div>
<div style={{ color: '#666' }}>Images</div>
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{stats.images}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<VideoCameraOutlined style={{ fontSize: 32, color: '#722ed1' }} />
<div>
<div style={{ color: '#666' }}>Videos</div>
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{stats.videos}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<FileOutlined style={{ fontSize: 32, color: '#fa8c16' }} />
<div>
<div style={{ color: '#666' }}>Total Size</div>
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{formatFileSize(stats.totalSize)}</div>
</div>
</div>
</Card>
</Col>
</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 }}
/>
<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>
)}
<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>
<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>
</Card>
<Modal
title="File Preview"
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
footer={null}
width={800}
>
{previewFile && (
<div>
{previewFile.type === 'image' ? (
<Image src={previewFile.url} style={{ width: '100%' }} />
) : (
<div style={{ textAlign: 'center', padding: 40 }}>
{TYPE_CONFIG[previewFile.type].icon}
<p style={{ marginTop: 16 }}>{previewFile.name}</p>
<p style={{ color: '#666' }}>{formatFileSize(previewFile.size)}</p>
<Button type="primary" href={previewFile.url} target="_blank">
Open File
</Button>
</div>
)}
</div>
)}
</Modal>
</div>
);
};
export default MaterialUpload;

View File

@@ -0,0 +1,404 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Descriptions,
Button,
Space,
Tag,
Image,
Divider,
Row,
Col,
Tabs,
Table,
Modal,
message,
Spin,
Typography,
Tooltip,
Badge,
} from 'antd';
import {
ShoppingCartOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined,
HistoryOutlined,
ShopOutlined,
TagOutlined,
DollarOutlined,
BoxPlotOutlined,
FileImageOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Title, Text } = Typography;
const { TabPane } = Tabs;
interface ProductDetail {
id: string;
productId: string;
platform: string;
name: string;
description: string;
category: string;
brand: string;
status: 'DRAFT' | 'PENDING' | 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
price: number;
costPrice: number;
currency: string;
stock: number;
sku: string;
images: string[];
attributes: Record<string, string>;
variants: ProductVariant[];
complianceStatus: 'COMPLIANT' | 'NON_COMPLIANT' | 'PENDING_REVIEW';
certificates: string[];
createdAt: string;
updatedAt: string;
publishedAt?: string;
}
interface ProductVariant {
id: string;
sku: string;
name: string;
price: number;
stock: number;
attributes: Record<string, string>;
}
interface PriceHistory {
id: string;
price: number;
currency: string;
changedAt: string;
changedBy: string;
reason: string;
}
interface StockHistory {
id: string;
quantity: number;
type: 'IN' | 'OUT';
reason: string;
createdAt: string;
}
const MOCK_PRODUCT: ProductDetail = {
id: '1',
productId: 'P-2026-001',
platform: 'Amazon',
name: 'Industrial Temperature Sensor Pro',
description: 'High-precision industrial temperature sensor with digital output. Suitable for manufacturing environments with temperature range -40°C to +125°C.',
category: 'Industrial Automation > Sensors',
brand: 'TechPro',
status: 'ACTIVE',
price: 89.99,
costPrice: 45.00,
currency: 'USD',
stock: 256,
sku: 'TP-TEMP-001',
images: [
'https://via.placeholder.com/400x400?text=Product+1',
'https://via.placeholder.com/400x400?text=Product+2',
'https://via.placeholder.com/400x400?text=Product+3',
],
attributes: {
'Temperature Range': '-40°C to +125°C',
'Accuracy': '±0.5°C',
'Output Type': 'Digital (I2C)',
'Power Supply': '3.3V - 5V DC',
'Housing Material': 'Stainless Steel 316',
'IP Rating': 'IP67',
},
variants: [
{ id: 'v1', sku: 'TP-TEMP-001-A', name: 'Standard Cable 1m', price: 89.99, stock: 150, attributes: { 'Cable Length': '1m' } },
{ id: 'v2', sku: 'TP-TEMP-001-B', name: 'Extended Cable 3m', price: 99.99, stock: 80, attributes: { 'Cable Length': '3m' } },
{ id: 'v3', sku: 'TP-TEMP-001-C', name: 'Extended Cable 5m', price: 109.99, stock: 26, attributes: { 'Cable Length': '5m' } },
],
complianceStatus: 'COMPLIANT',
certificates: ['CE', 'FCC', 'RoHS'],
createdAt: '2025-12-15 10:00:00',
updatedAt: '2026-03-18 14:30:00',
publishedAt: '2025-12-20 09:00:00',
};
const MOCK_PRICE_HISTORY: PriceHistory[] = [
{ id: '1', price: 89.99, currency: 'USD', changedAt: '2026-03-18 14:30:00', changedBy: 'System', reason: 'Market price adjustment' },
{ id: '2', price: 84.99, currency: 'USD', changedAt: '2026-02-01 10:00:00', changedBy: 'Admin', reason: 'Promotional pricing' },
{ id: '3', price: 94.99, currency: 'USD', changedAt: '2025-12-20 09:00:00', changedBy: 'Admin', reason: 'Initial listing price' },
];
const MOCK_STOCK_HISTORY: StockHistory[] = [
{ id: '1', quantity: 50, type: 'IN', reason: 'Restock from supplier', createdAt: '2026-03-15 08:00:00' },
{ id: '2', quantity: 25, type: 'OUT', reason: 'Order #ORD-2026-1234', createdAt: '2026-03-14 16:30:00' },
{ id: '3', quantity: 100, type: 'IN', reason: 'Initial stock', createdAt: '2025-12-20 09:00:00' },
];
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
DRAFT: { color: 'default', text: 'Draft' },
PENDING: { color: 'processing', text: 'Pending Review' },
ACTIVE: { color: 'success', text: 'Active' },
INACTIVE: { color: 'warning', text: 'Inactive' },
SUSPENDED: { color: 'error', text: 'Suspended' },
};
const COMPLIANCE_CONFIG: Record<string, { color: string; icon: React.ReactNode }> = {
COMPLIANT: { color: 'success', icon: <CheckCircleOutlined /> },
NON_COMPLIANT: { color: 'error', icon: <CloseCircleOutlined /> },
PENDING_REVIEW: { color: 'warning', icon: <SyncOutlined spin /> },
};
export const ProductDetail: React.FC = () => {
const [loading, setLoading] = useState(true);
const [product, setProduct] = useState<ProductDetail | null>(null);
const [priceHistory, setPriceHistory] = useState<PriceHistory[]>([]);
const [stockHistory, setStockHistory] = useState<StockHistory[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false);
useEffect(() => {
fetchProductDetail();
}, []);
const fetchProductDetail = async () => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 800));
setProduct(MOCK_PRODUCT);
setPriceHistory(MOCK_PRICE_HISTORY);
setStockHistory(MOCK_STOCK_HISTORY);
setLoading(false);
};
const handleEdit = () => {
setEditModalVisible(true);
};
const handleDelete = () => {
Modal.confirm({
title: 'Delete Product',
content: 'Are you sure you want to delete this product? This action cannot be undone.',
okText: 'Delete',
okType: 'danger',
onOk: () => {
message.success('Product deleted successfully');
},
});
};
const handleDuplicate = () => {
message.success('Product duplicated. Redirecting to edit page...');
};
const handlePublish = () => {
Modal.confirm({
title: 'Publish Product',
content: 'Publish this product to all connected platforms?',
onOk: () => {
message.success('Product published successfully');
},
});
};
const priceColumns: ColumnsType<PriceHistory> = [
{ title: 'Price', dataIndex: 'price', key: 'price', render: (v, r) => `${r.currency} ${v.toFixed(2)}` },
{ title: 'Changed At', dataIndex: 'changedAt', key: 'changedAt' },
{ title: 'Changed By', dataIndex: 'changedBy', key: 'changedBy' },
{ title: 'Reason', dataIndex: 'reason', key: 'reason' },
];
const stockColumns: ColumnsType<StockHistory> = [
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<Tag color={type === 'IN' ? 'green' : 'red'}>
{type === 'IN' ? 'Stock In' : 'Stock Out'}
</Tag>
)
},
{ title: 'Quantity', dataIndex: 'quantity', key: 'quantity' },
{ title: 'Reason', dataIndex: 'reason', key: 'reason' },
{ title: 'Date', dataIndex: 'createdAt', key: 'createdAt' },
];
const variantColumns: ColumnsType<ProductVariant> = [
{ title: 'SKU', dataIndex: 'sku', key: 'sku' },
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price', render: (v) => `USD ${v.toFixed(2)}` },
{
title: 'Stock',
dataIndex: 'stock',
key: 'stock',
render: (stock: number) => (
<Badge
count={stock}
showZero
style={{ backgroundColor: stock > 50 ? '#52c41a' : stock > 10 ? '#faad14' : '#ff4d4f' }}
/>
)
},
];
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" />
</div>
);
}
if (!product) {
return <div>Product not found</div>;
}
const profitMargin = ((product.price - product.costPrice) / product.price * 100).toFixed(1);
const statusConfig = STATUS_CONFIG[product.status];
const complianceConfig = COMPLIANCE_CONFIG[product.complianceStatus];
return (
<div className="product-detail-page">
<Card>
<Row gutter={24}>
<Col span={8}>
<Image.PreviewGroup>
<Image
src={product.images[0]}
alt={product.name}
style={{ width: '100%', borderRadius: 8 }}
/>
<div style={{ display: 'none' }}>
{product.images.slice(1).map((img, idx) => (
<Image key={idx} src={img} />
))}
</div>
</Image.PreviewGroup>
<Space style={{ marginTop: 16, width: '100%' }} wrap>
{product.images.map((img, idx) => (
<Image
key={idx}
src={img}
width={60}
height={60}
style={{ borderRadius: 4, objectFit: 'cover', cursor: 'pointer' }}
/>
))}
</Space>
</Col>
<Col span={16}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div>
<Space>
<Tag color="blue">{product.platform}</Tag>
<Tag color={statusConfig.color}>{statusConfig.text}</Tag>
<Tag color={complianceConfig.color} icon={complianceConfig.icon}>
{product.complianceStatus.replace('_', ' ')}
</Tag>
</Space>
<Title level={3} style={{ marginTop: 8, marginBottom: 0 }}>{product.name}</Title>
<Text type="secondary">SKU: {product.sku} | ID: {product.productId}</Text>
</div>
<Descriptions column={2} size="small">
<Descriptions.Item label="Category">{product.category}</Descriptions.Item>
<Descriptions.Item label="Brand">{product.brand}</Descriptions.Item>
<Descriptions.Item label={<><DollarOutlined /> Price</>}>
<Text strong style={{ fontSize: 18 }}>{product.currency} {product.price.toFixed(2)}</Text>
</Descriptions.Item>
<Descriptions.Item label="Cost Price">
{product.currency} {product.costPrice.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="Profit Margin">
<Tag color={parseFloat(profitMargin) >= 30 ? 'green' : parseFloat(profitMargin) >= 15 ? 'orange' : 'red'}>
{profitMargin}%
</Tag>
</Descriptions.Item>
<Descriptions.Item label={<><BoxPlotOutlined /> Stock</>}>
<Badge
count={product.stock}
showZero
style={{ backgroundColor: product.stock > 50 ? '#52c41a' : product.stock > 10 ? '#faad14' : '#ff4d4f' }}
/>
</Descriptions.Item>
</Descriptions>
<div>
<Text strong>Certificates: </Text>
{product.certificates.map(cert => (
<Tag key={cert} icon={<CheckCircleOutlined />} color="green">{cert}</Tag>
))}
</div>
<Divider style={{ margin: '12px 0' }} />
<Space>
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>Edit</Button>
<Button icon={<CopyOutlined />} onClick={handleDuplicate}>Duplicate</Button>
<Button icon={<ShopOutlined />} onClick={handlePublish}>Publish</Button>
<Button danger icon={<DeleteOutlined />} onClick={handleDelete}>Delete</Button>
</Space>
</Space>
</Col>
</Row>
</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>
</Card>
<Modal
title="Edit Product"
open={editModalVisible}
onCancel={() => setEditModalVisible(false)}
footer={null}
width={800}
>
<p>Edit form will be implemented here...</p>
</Modal>
</div>
);
};
export default ProductDetail;

View File

@@ -0,0 +1,465 @@
import React, { useState } from 'react';
import {
Card,
Form,
Input,
Select,
InputNumber,
Button,
Space,
Row,
Col,
Divider,
Upload,
message,
Tabs,
Table,
Tag,
Modal,
Switch,
Tooltip,
Alert,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
SaveOutlined,
SendOutlined,
InfoCircleOutlined,
ShopOutlined,
DollarOutlined,
BoxPlotOutlined,
} from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
const { Option } = Select;
const { TabPane } = Tabs;
const { TextArea } = Input;
interface ProductVariant {
id: string;
sku: string;
name: string;
price: number;
stock: number;
attributes: Record<string, string>;
}
interface PublishFormValues {
name: string;
platform: string[];
category: string;
brand: string;
description: string;
price: number;
costPrice: number;
currency: string;
stock: number;
sku: string;
status: 'DRAFT' | 'PENDING' | 'ACTIVE';
autoPublish: boolean;
complianceCheck: boolean;
}
const PLATFORMS = [
{ value: 'Amazon', label: 'Amazon' },
{ value: 'eBay', label: 'eBay' },
{ value: 'Shopify', label: 'Shopify' },
{ value: 'Walmart', label: 'Walmart' },
{ value: 'AliExpress', label: 'AliExpress' },
];
const CATEGORIES = [
{ value: 'Industrial Automation > Sensors', label: 'Industrial Automation > Sensors' },
{ value: 'Industrial Automation > Controllers', label: 'Industrial Automation > Controllers' },
{ value: 'Industrial Automation > Motors', label: 'Industrial Automation > Motors' },
{ value: 'Electronics > Components', label: 'Electronics > Components' },
{ value: 'Tools > Power Tools', label: 'Tools > Power Tools' },
];
const CURRENCIES = [
{ value: 'USD', label: 'USD - US Dollar' },
{ value: 'EUR', label: 'EUR - Euro' },
{ value: 'GBP', label: 'GBP - British Pound' },
{ value: 'CNY', label: 'CNY - Chinese Yuan' },
];
export const ProductPublishForm: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [variants, setVariants] = useState<ProductVariant[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [activeTab, setActiveTab] = useState('basic');
const handleAddVariant = () => {
const newVariant: ProductVariant = {
id: `v${Date.now()}`,
sku: '',
name: '',
price: 0,
stock: 0,
attributes: {},
};
setVariants([...variants, newVariant]);
};
const handleRemoveVariant = (id: string) => {
setVariants(variants.filter(v => v.id !== id));
};
const handleVariantChange = (id: string, field: string, value: any) => {
setVariants(variants.map(v =>
v.id === id ? { ...v, [field]: value } : v
));
};
const handleSaveDraft = async () => {
try {
await form.validateFields();
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
message.success('Draft saved successfully');
} catch (error) {
message.error('Please fill in required fields');
} finally {
setLoading(false);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
if (values.complianceCheck) {
await new Promise(resolve => setTimeout(resolve, 500));
message.info('Running compliance check...');
}
await new Promise(resolve => setTimeout(resolve, 1000));
Modal.success({
title: 'Product Submitted',
content: `Product "${values.name}" has been submitted for ${values.autoPublish ? 'auto-publish' : 'review'} on ${values.platform.join(', ')}.`,
});
} catch (error) {
message.error('Please fill in required fields');
} finally {
setLoading(false);
}
};
const variantColumns = [
{
title: 'SKU',
dataIndex: 'sku',
key: 'sku',
render: (value: string, record: ProductVariant) => (
<Input
value={value}
onChange={(e) => handleVariantChange(record.id, 'sku', e.target.value)}
placeholder="SKU"
/>
),
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (value: string, record: ProductVariant) => (
<Input
value={value}
onChange={(e) => handleVariantChange(record.id, 'name', e.target.value)}
placeholder="Variant Name"
/>
),
},
{
title: 'Price',
dataIndex: 'price',
key: 'price',
render: (value: number, record: ProductVariant) => (
<InputNumber
value={value}
onChange={(v) => handleVariantChange(record.id, 'price', v)}
min={0}
precision={2}
style={{ width: '100%' }}
/>
),
},
{
title: 'Stock',
dataIndex: 'stock',
key: 'stock',
render: (value: number, record: ProductVariant) => (
<InputNumber
value={value}
onChange={(v) => handleVariantChange(record.id, 'stock', v)}
min={0}
style={{ width: '100%' }}
/>
),
},
{
title: 'Action',
key: 'action',
render: (_: any, record: ProductVariant) => (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveVariant(record.id)}
/>
),
},
];
return (
<div className="product-publish-form-page">
<Card title="Product Publish Form">
<Form
form={form}
layout="vertical"
initialValues={{
currency: 'USD',
status: 'DRAFT',
autoPublish: false,
complianceCheck: true,
}}
>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Basic Info" key="basic">
<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="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>All products must pass compliance check before publishing</li>
<li>Products with profit margin below threshold will require manual approval</li>
<li>Images must meet platform-specific requirements</li>
</ul>
}
type="warning"
showIcon
/>
</TabPane>
</Tabs>
<Divider />
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => form.resetFields()}>Reset</Button>
<Button icon={<SaveOutlined />} onClick={handleSaveDraft} loading={loading}>
Save Draft
</Button>
<Button type="primary" icon={<SendOutlined />} onClick={handleSubmit} loading={loading}>
Submit for Review
</Button>
</Space>
</Form>
</Card>
</div>
);
};
export default ProductPublishForm;

View File

@@ -0,0 +1,3 @@
export { ProductDetail } from './ProductDetail';
export { ProductPublishForm } from './ProductPublishForm';
export { MaterialUpload } from './MaterialUpload';