feat: 添加MSW模拟服务和数据源集成
refactor: 重构页面组件移除冗余Layout组件 feat: 实现WebSocket和事件总线系统 feat: 添加队列和调度系统 docs: 更新架构文档和服务映射 style: 清理重复接口定义使用数据源 chore: 更新依赖项配置 feat: 添加运行时系统和领域引导 ci: 配置ESLint边界检查规则 build: 添加Redis和WebSocket依赖 test: 添加MSW浏览器环境入口 perf: 优化数据获取逻辑使用统一数据源 fix: 修复类型定义和状态管理问题
This commit is contained in:
640
dashboard/src/pages/StrategyMarketplace/index.tsx
Normal file
640
dashboard/src/pages/StrategyMarketplace/index.tsx
Normal file
@@ -0,0 +1,640 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Tabs,
|
||||
Table,
|
||||
Tag,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Spin,
|
||||
message,
|
||||
Select,
|
||||
Button,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Input,
|
||||
Modal,
|
||||
Progress,
|
||||
Rate,
|
||||
Empty,
|
||||
Typography,
|
||||
Space,
|
||||
Divider
|
||||
} from 'antd';
|
||||
import {
|
||||
ShopOutlined,
|
||||
ThunderboltOutlined,
|
||||
FireOutlined,
|
||||
StarOutlined,
|
||||
SearchOutlined,
|
||||
RocketOutlined,
|
||||
DollarOutlined,
|
||||
PercentageOutlined,
|
||||
BarChartOutlined,
|
||||
SettingOutlined,
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
BulbOutlined,
|
||||
TagOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
interface Strategy {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'PRICING' | 'ADVERTISING' | 'PRODUCT_SELECTION' | 'INVENTORY' | 'LOGISTICS' | 'MARKETING';
|
||||
risk_level: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
price: number;
|
||||
billing_type: 'FREE' | 'ONE_TIME' | 'SUBSCRIPTION' | 'USAGE_BASED';
|
||||
avg_roi: number;
|
||||
usage_count: number;
|
||||
success_rate: number;
|
||||
tags: string[];
|
||||
is_active: boolean;
|
||||
is_featured: boolean;
|
||||
}
|
||||
|
||||
interface MerchantStrategy {
|
||||
id: string;
|
||||
strategy_id: string;
|
||||
status: 'ACTIVE' | 'PAUSED' | 'COMPLETED' | 'FAILED';
|
||||
roi_achieved: number;
|
||||
revenue_generated: number;
|
||||
activated_at: string;
|
||||
strategy: Strategy;
|
||||
}
|
||||
|
||||
interface Recommendation {
|
||||
strategy: Strategy;
|
||||
score: number;
|
||||
reasons: string[];
|
||||
expectedRoi: number;
|
||||
confidence: number;
|
||||
priority: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
PRICING: 'blue',
|
||||
ADVERTISING: 'orange',
|
||||
PRODUCT_SELECTION: 'green',
|
||||
INVENTORY: 'purple',
|
||||
LOGISTICS: 'cyan',
|
||||
MARKETING: 'magenta'
|
||||
};
|
||||
|
||||
const categoryIcons: Record<string, React.ReactNode> = {
|
||||
PRICING: <DollarOutlined />,
|
||||
ADVERTISING: <ThunderboltOutlined />,
|
||||
PRODUCT_SELECTION: <ShopOutlined />,
|
||||
INVENTORY: <BarChartOutlined />,
|
||||
LOGISTICS: <RocketOutlined />,
|
||||
MARKETING: <BulbOutlined />
|
||||
};
|
||||
|
||||
const riskColors: Record<string, string> = {
|
||||
LOW: 'success',
|
||||
MEDIUM: 'warning',
|
||||
HIGH: 'error'
|
||||
};
|
||||
|
||||
const StrategyMarketplacePage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('marketplace');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [strategies, setStrategies] = useState<Strategy[]>([]);
|
||||
const [featuredStrategies, setFeaturedStrategies] = useState<Strategy[]>([]);
|
||||
const [trendingStrategies, setTrendingStrategies] = useState<Strategy[]>([]);
|
||||
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
||||
const [myStrategies, setMyStrategies] = useState<MerchantStrategy[]>([]);
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<Strategy | null>(null);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [strategiesRes, featuredRes, trendingRes, recommendationsRes, myStrategiesRes] = await Promise.all([
|
||||
fetch('/api/v1/strategy/strategies?isActive=true').then(r => r.json()),
|
||||
fetch('/api/v1/strategy/strategies/featured').then(r => r.json()),
|
||||
fetch('/api/v1/strategy/strategies/trending').then(r => r.json()),
|
||||
fetch('/api/v1/strategy/recommendations').then(r => r.json()),
|
||||
fetch('/api/v1/strategy/my-strategies').then(r => r.json())
|
||||
]);
|
||||
|
||||
if (strategiesRes.success) setStrategies(strategiesRes.data);
|
||||
if (featuredRes.success) setFeaturedStrategies(featuredRes.data);
|
||||
if (trendingRes.success) setTrendingStrategies(trendingRes.data);
|
||||
if (recommendationsRes.success) setRecommendations(recommendationsRes.data);
|
||||
if (myStrategiesRes.success) setMyStrategies(myStrategiesRes.data);
|
||||
} catch (error) {
|
||||
message.error('Failed to load strategies');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
if (!value.trim()) {
|
||||
fetchInitialData();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/strategy/strategies/search?q=${encodeURIComponent(value)}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setStrategies(data.data);
|
||||
setActiveTab('marketplace');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Search failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateStrategy = async (strategyId: string, config?: Record<string, any>) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/strategy/strategies/${strategyId}/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ config })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
message.success('Strategy activated successfully!');
|
||||
fetchInitialData();
|
||||
setDetailModalVisible(false);
|
||||
} else {
|
||||
message.error(data.message || 'Failed to activate strategy');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Failed to activate strategy');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePauseStrategy = async (merchantStrategyId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/strategy/my-strategies/${merchantStrategyId}/pause`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
message.success('Strategy paused');
|
||||
fetchInitialData();
|
||||
} else {
|
||||
message.error(data.message || 'Failed to pause strategy');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Failed to pause strategy');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumeStrategy = async (merchantStrategyId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/strategy/my-strategies/${merchantStrategyId}/resume`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
message.success('Strategy resumed');
|
||||
fetchInitialData();
|
||||
} else {
|
||||
message.error(data.message || 'Failed to resume strategy');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Failed to resume strategy');
|
||||
}
|
||||
};
|
||||
|
||||
const showStrategyDetail = (strategy: Strategy) => {
|
||||
setSelectedStrategy(strategy);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const filteredStrategies = categoryFilter === 'all'
|
||||
? strategies
|
||||
: strategies.filter(s => s.category === categoryFilter);
|
||||
|
||||
const renderStrategyCard = (strategy: Strategy, showActions: boolean = true) => (
|
||||
<Card
|
||||
hoverable
|
||||
style={{ marginBottom: 16 }}
|
||||
onClick={() => showStrategyDetail(strategy)}
|
||||
>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="auto">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Text strong style={{ fontSize: 16 }}>{strategy.name}</Text>
|
||||
{strategy.is_featured && <Tag icon={<StarOutlined />} color="gold">Featured</Tag>}
|
||||
</Space>
|
||||
<Space>
|
||||
<Tag color={categoryColors[strategy.category]} icon={categoryIcons[strategy.category]}>
|
||||
{strategy.category.replace('_', ' ')}
|
||||
</Tag>
|
||||
<Tag color={riskColors[strategy.risk_level]}>{strategy.risk_level} Risk</Tag>
|
||||
{strategy.price === 0 ? (
|
||||
<Tag color="green">FREE</Tag>
|
||||
) : (
|
||||
<Tag color="blue">${strategy.price.toFixed(2)}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
<Text type="secondary" ellipsis style={{ maxWidth: 400 }}>
|
||||
{strategy.description}
|
||||
</Text>
|
||||
<Space>
|
||||
<Text type="secondary">
|
||||
<PercentageOutlined /> Avg ROI: {(strategy.avg_roi * 100).toFixed(1)}%
|
||||
</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">
|
||||
<CheckCircleOutlined /> Success: {strategy.success_rate}%
|
||||
</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">
|
||||
<BarChartOutlined /> Used: {strategy.usage_count}
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
{showActions && (
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleActivateStrategy(strategy.id);
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderRecommendationCard = (rec: Recommendation) => (
|
||||
<Card
|
||||
hoverable
|
||||
style={{ marginBottom: 16 }}
|
||||
onClick={() => showStrategyDetail(rec.strategy)}
|
||||
>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="auto">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Text strong style={{ fontSize: 16 }}>{rec.strategy.name}</Text>
|
||||
<Tag color={rec.priority === 'HIGH' ? 'red' : rec.priority === 'MEDIUM' ? 'orange' : 'blue'}>
|
||||
{rec.priority} Priority
|
||||
</Tag>
|
||||
<Badge count={`Score: ${rec.score.toFixed(0)}`} style={{ backgroundColor: '#52c41a' }} />
|
||||
</Space>
|
||||
<Space wrap>
|
||||
{rec.reasons.map((reason, idx) => (
|
||||
<Tag key={idx} icon={<BulbOutlined />} color="processing">{reason}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
<Space>
|
||||
<Text type="secondary">
|
||||
Expected ROI: <Text strong style={{ color: '#52c41a' }}>{(rec.expectedRoi * 100).toFixed(1)}%</Text>
|
||||
</Text>
|
||||
<Divider type="vertical" />
|
||||
<Text type="secondary">
|
||||
Confidence: <Progress percent={rec.confidence * 100} size="small" style={{ width: 80, display: 'inline-block' }} />
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleActivateStrategy(rec.strategy.id);
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const myStrategiesColumns: ColumnsType<MerchantStrategy> = [
|
||||
{
|
||||
title: 'Strategy',
|
||||
dataIndex: ['strategy', 'name'],
|
||||
key: 'name',
|
||||
render: (name: string, record) => (
|
||||
<Space>
|
||||
<Tag color={categoryColors[record.strategy.category]} icon={categoryIcons[record.strategy.category]}>
|
||||
{record.strategy.category.replace('_', ' ')}
|
||||
</Tag>
|
||||
<Text strong>{name}</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const statusConfig: Record<string, { color: string; icon: React.ReactNode }> = {
|
||||
ACTIVE: { color: 'success', icon: <CheckCircleOutlined /> },
|
||||
PAUSED: { color: 'warning', icon: <PauseCircleOutlined /> },
|
||||
COMPLETED: { color: 'processing', icon: <CheckCircleOutlined /> },
|
||||
FAILED: { color: 'error', icon: <CloseCircleOutlined /> }
|
||||
};
|
||||
const config = statusConfig[status];
|
||||
return <Tag color={config.color} icon={config.icon}>{status}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'ROI Achieved',
|
||||
dataIndex: 'roi_achieved',
|
||||
key: 'roi',
|
||||
render: (roi: number) => (
|
||||
<Text style={{ color: roi > 0 ? '#52c41a' : '#ff4d4f' }}>
|
||||
{(roi * 100).toFixed(1)}%
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Revenue Generated',
|
||||
dataIndex: 'revenue_generated',
|
||||
key: 'revenue',
|
||||
render: (revenue: number) => `$${revenue.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
title: 'Activated',
|
||||
dataIndex: 'activated_at',
|
||||
key: 'activated_at',
|
||||
render: (date: string) => new Date(date).toLocaleDateString()
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
{record.status === 'ACTIVE' && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PauseCircleOutlined />}
|
||||
onClick={() => handlePauseStrategy(record.id)}
|
||||
>
|
||||
Pause
|
||||
</Button>
|
||||
)}
|
||||
{record.status === 'PAUSED' && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleResumeStrategy(record.id)}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const renderDetailModal = () => (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
{selectedStrategy?.name}
|
||||
{selectedStrategy?.is_featured && <Tag icon={<StarOutlined />} color="gold">Featured</Tag>}
|
||||
</Space>
|
||||
}
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setDetailModalVisible(false)}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="activate"
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleActivateStrategy(selectedStrategy?.id || '')}
|
||||
>
|
||||
Activate Strategy
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{selectedStrategy && (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Paragraph>{selectedStrategy.description}</Paragraph>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Average ROI"
|
||||
value={(selectedStrategy.avg_roi * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Success Rate"
|
||||
value={selectedStrategy.success_rate}
|
||||
suffix="%"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="Usage Count"
|
||||
value={selectedStrategy.usage_count}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">Category: </Text>
|
||||
<Tag color={categoryColors[selectedStrategy.category]} icon={categoryIcons[selectedStrategy.category]}>
|
||||
{selectedStrategy.category.replace('_', ' ')}
|
||||
</Tag>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">Risk Level: </Text>
|
||||
<Tag color={riskColors[selectedStrategy.risk_level]}>
|
||||
{selectedStrategy.risk_level}
|
||||
</Tag>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">Price: </Text>
|
||||
{selectedStrategy.price === 0 ? (
|
||||
<Tag color="green">FREE</Tag>
|
||||
) : (
|
||||
<Text strong>${selectedStrategy.price.toFixed(2)}</Text>
|
||||
)}
|
||||
<Text type="secondary"> ({selectedStrategy.billing_type})</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<Text type="secondary">Tags: </Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
{selectedStrategy.tags.map((tag, idx) => (
|
||||
<Tag key={idx} icon={<TagOutlined />}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col flex="auto">
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
<ShopOutlined /> Strategy Marketplace
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
Discover and activate AI-powered strategies to grow your business
|
||||
</Text>
|
||||
</Col>
|
||||
<Col>
|
||||
<Search
|
||||
placeholder="Search strategies..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane
|
||||
tab={<span><ShopOutlined /> Marketplace</span>}
|
||||
key="marketplace"
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Select
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
>
|
||||
<Option value="all">All Categories</Option>
|
||||
<Option value="PRICING">Pricing</Option>
|
||||
<Option value="ADVERTISING">Advertising</Option>
|
||||
<Option value="PRODUCT_SELECTION">Product Selection</Option>
|
||||
<Option value="INVENTORY">Inventory</Option>
|
||||
<Option value="LOGISTICS">Logistics</Option>
|
||||
<Option value="MARKETING">Marketing</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{filteredStrategies.length === 0 ? (
|
||||
<Empty description="No strategies found" />
|
||||
) : (
|
||||
filteredStrategies.map(strategy => renderStrategyCard(strategy))
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={<span><StarOutlined /> Featured</span>}
|
||||
key="featured"
|
||||
>
|
||||
{featuredStrategies.length === 0 ? (
|
||||
<Empty description="No featured strategies" />
|
||||
) : (
|
||||
featuredStrategies.map(strategy => renderStrategyCard(strategy))
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={<span><FireOutlined /> Trending</span>}
|
||||
key="trending"
|
||||
>
|
||||
{trendingStrategies.length === 0 ? (
|
||||
<Empty description="No trending strategies" />
|
||||
) : (
|
||||
trendingStrategies.map(strategy => renderStrategyCard(strategy))
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={<span><BulbOutlined /> Recommendations</span>}
|
||||
key="recommendations"
|
||||
>
|
||||
{recommendations.length === 0 ? (
|
||||
<Empty description="No recommendations available" />
|
||||
) : (
|
||||
recommendations.map(rec => renderRecommendationCard(rec))
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={<span><SettingOutlined /> My Strategies</span>}
|
||||
key="my-strategies"
|
||||
>
|
||||
<Table
|
||||
columns={myStrategiesColumns}
|
||||
dataSource={myStrategies}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{renderDetailModal()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyMarketplacePage;
|
||||
Reference in New Issue
Block a user