feat(types): 添加express.d.ts类型引用 style: 格式化express.d.ts中的接口定义 refactor: 移除未使用的AntFC类型导入 chore: 删除自动生成的.umi-production文件 feat: 添加店铺管理相关表和初始化脚本 docs: 更新安全规则和交互指南文档 refactor: 统一使用FC类型替代React.FC perf: 优化图表组件导入方式 style: 添加.prettierrc配置文件 refactor: 调整组件导入顺序和结构 feat: 添加平台库存管理路由 fix: 修复订单同步时的库存检查逻辑 docs: 更新RBAC设计和租户管理文档 refactor: 优化部门控制器代码
630 lines
21 KiB
TypeScript
630 lines
21 KiB
TypeScript
import { useLocale } from '@/contexts/LocaleContext';
|
|
import {
|
|
useState,
|
|
useEffect,
|
|
Card,
|
|
Tabs,
|
|
Table,
|
|
Tag,
|
|
Statistic,
|
|
Row,
|
|
Col,
|
|
Spin,
|
|
message,
|
|
Select,
|
|
Button,
|
|
Tooltip,
|
|
Badge,
|
|
Input,
|
|
Modal,
|
|
Progress,
|
|
Rate,
|
|
Empty,
|
|
Typography,
|
|
Space,
|
|
Divider,
|
|
ShopOutlined,
|
|
ThunderboltOutlined,
|
|
FireOutlined,
|
|
StarOutlined,
|
|
SearchOutlined,
|
|
RocketOutlined,
|
|
DollarOutlined,
|
|
PercentageOutlined,
|
|
BarChartOutlined,
|
|
SettingOutlined,
|
|
PlayCircleOutlined,
|
|
PauseCircleOutlined,
|
|
CheckCircleOutlined,
|
|
CloseCircleOutlined,
|
|
BulbOutlined,
|
|
TagOutlined,
|
|
Option,
|
|
Search,
|
|
Title,
|
|
Text,
|
|
Paragraph,
|
|
FC,
|
|
} from '@/imports';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
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: FC = () => {
|
|
const { t } = useLocale();
|
|
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">{t('strategyMarketplace.featured')}</Tag>}
|
|
</Space>
|
|
<Space>
|
|
<Tag color={categoryColors[strategy.category]} icon={categoryIcons[strategy.category]}>
|
|
{t(`strategyMarketplace.categories.${strategy.category.toLowerCase()}`)}
|
|
</Tag>
|
|
<Tag color={riskColors[strategy.risk_level]}>{t(`strategyMarketplace.risk.${strategy.risk_level.toLowerCase()}`)}</Tag>
|
|
{strategy.price === 0 ? (
|
|
<Tag color="green">{t('strategyMarketplace.price.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 /> {t('strategyMarketplace.stats.avgRoi')}: {(strategy.avg_roi * 100).toFixed(1)}%
|
|
</Text>
|
|
<Divider type="vertical" />
|
|
<Text type="secondary">
|
|
<CheckCircleOutlined /> {t('strategyMarketplace.stats.success')}: {strategy.success_rate}%
|
|
</Text>
|
|
<Divider type="vertical" />
|
|
<Text type="secondary">
|
|
<BarChartOutlined /> {t('strategyMarketplace.stats.used')}: {strategy.usage_count}
|
|
</Text>
|
|
</Space>
|
|
</Space>
|
|
</Col>
|
|
{showActions && (
|
|
<Col>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleActivateStrategy(strategy.id);
|
|
}}
|
|
>
|
|
{t('strategyMarketplace.actions.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'}>
|
|
{t(`strategyMarketplace.priority.${rec.priority.toLowerCase()}`)}
|
|
</Tag>
|
|
<Badge count={`${t('strategyMarketplace.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">
|
|
{t('strategyMarketplace.expectedRoi')}: <Text strong style={{ color: '#52c41a' }}>{(rec.expectedRoi * 100).toFixed(1)}%</Text>
|
|
</Text>
|
|
<Divider type="vertical" />
|
|
<Text type="secondary">
|
|
{t('strategyMarketplace.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);
|
|
}}
|
|
>
|
|
{t('strategyMarketplace.actions.activate')}
|
|
</Button>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
);
|
|
|
|
const myStrategiesColumns: ColumnsType<MerchantStrategy> = [
|
|
{
|
|
title: t('strategyMarketplace.columns.strategy'),
|
|
dataIndex: ['strategy', 'name'],
|
|
key: 'name',
|
|
render: (name: string, record) => (
|
|
<Space>
|
|
<Tag color={categoryColors[record.strategy.category]} icon={categoryIcons[record.strategy.category]}>
|
|
{t(`strategyMarketplace.categories.${record.strategy.category.toLowerCase()}`)}
|
|
</Tag>
|
|
<Text strong>{name}</Text>
|
|
</Space>
|
|
)
|
|
},
|
|
{
|
|
title: t('strategyMarketplace.columns.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}>{t(`strategyMarketplace.status.${status.toLowerCase()}`)}</Tag>;
|
|
}
|
|
},
|
|
{
|
|
title: t('strategyMarketplace.columns.roiAchieved'),
|
|
dataIndex: 'roi_achieved',
|
|
key: 'roi',
|
|
render: (roi: number) => (
|
|
<Text style={{ color: roi > 0 ? '#52c41a' : '#ff4d4f' }}>
|
|
{(roi * 100).toFixed(1)}%
|
|
</Text>
|
|
)
|
|
},
|
|
{
|
|
title: t('strategyMarketplace.columns.revenueGenerated'),
|
|
dataIndex: 'revenue_generated',
|
|
key: 'revenue',
|
|
render: (revenue: number) => `$${revenue.toFixed(2)}`
|
|
},
|
|
{
|
|
title: t('strategyMarketplace.columns.activated'),
|
|
dataIndex: 'activated_at',
|
|
key: 'activated_at',
|
|
render: (date: string) => new Date(date).toLocaleDateString()
|
|
},
|
|
{
|
|
title: t('strategyMarketplace.columns.actions'),
|
|
key: 'actions',
|
|
render: (_, record) => (
|
|
<Space>
|
|
{record.status === 'ACTIVE' && (
|
|
<Button
|
|
size="small"
|
|
icon={<PauseCircleOutlined />}
|
|
onClick={() => handlePauseStrategy(record.id)}
|
|
>
|
|
{t('strategyMarketplace.actions.pause')}
|
|
</Button>
|
|
)}
|
|
{record.status === 'PAUSED' && (
|
|
<Button
|
|
size="small"
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={() => handleResumeStrategy(record.id)}
|
|
>
|
|
{t('strategyMarketplace.actions.resume')}
|
|
</Button>
|
|
)}
|
|
</Space>
|
|
)
|
|
}
|
|
];
|
|
|
|
const renderDetailModal = () => (
|
|
<Modal
|
|
title={
|
|
<Space>
|
|
{selectedStrategy?.name}
|
|
{selectedStrategy?.is_featured && <Tag icon={<StarOutlined />} color="gold">{t('strategyMarketplace.featured')}</Tag>}
|
|
</Space>
|
|
}
|
|
open={detailModalVisible}
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
width={700}
|
|
footer={[
|
|
<Button key="cancel" onClick={() => setDetailModalVisible(false)}>
|
|
{t('strategyMarketplace.actions.cancel')}
|
|
</Button>,
|
|
<Button
|
|
key="activate"
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={() => handleActivateStrategy(selectedStrategy?.id || '')}
|
|
>
|
|
{t('strategyMarketplace.actions.activateStrategy')}
|
|
</Button>
|
|
]}
|
|
>
|
|
{selectedStrategy && (
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
<Paragraph>{selectedStrategy.description}</Paragraph>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title={t('strategyMarketplace.stats.averageRoi')}
|
|
value={(selectedStrategy.avg_roi * 100).toFixed(1)}
|
|
suffix="%"
|
|
valueStyle={{ color: '#52c41a' }}
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title={t('strategyMarketplace.stats.successRate')}
|
|
value={selectedStrategy.success_rate}
|
|
suffix="%"
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title={t('strategyMarketplace.stats.usageCount')}
|
|
value={selectedStrategy.usage_count}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider />
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Text type="secondary">{t('strategyMarketplace.category')}: </Text>
|
|
<Tag color={categoryColors[selectedStrategy.category]} icon={categoryIcons[selectedStrategy.category]}>
|
|
{t(`strategyMarketplace.categories.${selectedStrategy.category.toLowerCase()}`)}
|
|
</Tag>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Text type="secondary">{t('strategyMarketplace.riskLevel')}: </Text>
|
|
<Tag color={riskColors[selectedStrategy.risk_level]}>
|
|
{t(`strategyMarketplace.risk.${selectedStrategy.risk_level.toLowerCase()}`)}
|
|
</Tag>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Text type="secondary">{t('strategyMarketplace.price.title')}: </Text>
|
|
{selectedStrategy.price === 0 ? (
|
|
<Tag color="green">{t('strategyMarketplace.price.free')}</Tag>
|
|
) : (
|
|
<Text strong>${selectedStrategy.price.toFixed(2)}</Text>
|
|
)}
|
|
<Text type="secondary"> ({t(`strategyMarketplace.billingType.${selectedStrategy.billing_type.toLowerCase()}`)})</Text>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider />
|
|
|
|
<div>
|
|
<Text type="secondary">{t('strategyMarketplace.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 /> {t('strategyMarketplace.title')}
|
|
</Title>
|
|
<Text type="secondary">
|
|
{t('strategyMarketplace.description')}
|
|
</Text>
|
|
</Col>
|
|
<Col>
|
|
<Search
|
|
placeholder={t('strategyMarketplace.searchPlaceholder')}
|
|
allowClear
|
|
enterButton={<SearchOutlined />}
|
|
style={{ width: 300 }}
|
|
onSearch={handleSearch}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
</Col>
|
|
|
|
<Col span={24}>
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={setActiveTab}
|
|
items={[
|
|
{ key: 'marketplace', label: <span><ShopOutlined /> {t('strategyMarketplace.tabs.marketplace')}</span>, children: (
|
|
<>
|
|
<Row gutter={16}>
|
|
<Col span={6}>
|
|
<Select
|
|
style={{ width: '100%', marginBottom: 16 }}
|
|
value={categoryFilter}
|
|
onChange={setCategoryFilter}
|
|
>
|
|
<Option value="all">{t('strategyMarketplace.categories.all')}</Option>
|
|
<Option value="PRICING">{t('strategyMarketplace.categories.pricing')}</Option>
|
|
<Option value="ADVERTISING">{t('strategyMarketplace.categories.advertising')}</Option>
|
|
<Option value="PRODUCT_SELECTION">{t('strategyMarketplace.categories.productSelection')}</Option>
|
|
<Option value="INVENTORY">{t('strategyMarketplace.categories.inventory')}</Option>
|
|
<Option value="LOGISTICS">{t('strategyMarketplace.categories.logistics')}</Option>
|
|
<Option value="MARKETING">{t('strategyMarketplace.categories.marketing')}</Option>
|
|
</Select>
|
|
</Col>
|
|
</Row>
|
|
|
|
{filteredStrategies.length === 0 ? (
|
|
<Empty description={t('strategyMarketplace.noStrategies')} />
|
|
) : (
|
|
filteredStrategies.map(strategy => renderStrategyCard(strategy))
|
|
)}
|
|
</>
|
|
)},
|
|
{ key: 'featured', label: <span><StarOutlined /> {t('strategyMarketplace.tabs.featured')}</span>, children: (
|
|
featuredStrategies.length === 0 ? (
|
|
<Empty description={t('strategyMarketplace.noFeaturedStrategies')} />
|
|
) : (
|
|
featuredStrategies.map(strategy => renderStrategyCard(strategy))
|
|
)
|
|
)},
|
|
{ key: 'trending', label: <span><FireOutlined /> {t('strategyMarketplace.tabs.trending')}</span>, children: (
|
|
trendingStrategies.length === 0 ? (
|
|
<Empty description={t('strategyMarketplace.noTrendingStrategies')} />
|
|
) : (
|
|
trendingStrategies.map(strategy => renderStrategyCard(strategy))
|
|
)
|
|
)},
|
|
{ key: 'recommendations', label: <span><BulbOutlined /> {t('strategyMarketplace.tabs.recommendations')}</span>, children: (
|
|
recommendations.length === 0 ? (
|
|
<Empty description={t('strategyMarketplace.noRecommendations')} />
|
|
) : (
|
|
recommendations.map(rec => renderRecommendationCard(rec))
|
|
)
|
|
)},
|
|
{ key: 'my-strategies', label: <span><SettingOutlined /> {t('strategyMarketplace.tabs.myStrategies')}</span>, children: (
|
|
<Table
|
|
columns={myStrategiesColumns}
|
|
dataSource={myStrategies}
|
|
rowKey="id"
|
|
pagination={{ pageSize: 10 }}
|
|
/>
|
|
)},
|
|
]}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
{renderDetailModal()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StrategyMarketplacePage;
|