feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
This commit is contained in:
404
dashboard/src/pages/Product/ProductDetail.tsx
Normal file
404
dashboard/src/pages/Product/ProductDetail.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user