import { useState, useEffect } from 'react' import type { AIModel, Exchange, CreateTraderRequest, Strategy } from '../types' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { toast } from 'sonner' import { Pencil, Plus, X as IconX, Sparkles, ExternalLink, UserPlus } from 'lucide-react' import { httpClient } from '../lib/httpClient' // 提取下划线后面的名称部分 function getShortName(fullName: string): string { const parts = fullName.split('_') return parts.length > 1 ? parts[parts.length - 1] : fullName } // 交易所注册链接配置 const EXCHANGE_REGISTRATION_LINKS: Record = { binance: { url: 'https://www.binance.com/join?ref=NOFXENG', hasReferral: true }, okx: { url: 'https://www.okx.com/join/1865360', hasReferral: true }, bybit: { url: 'https://partner.bybit.com/b/83856', hasReferral: true }, hyperliquid: { url: 'https://app.hyperliquid.xyz/join/AITRADING', hasReferral: true }, aster: { url: 'https://www.asterdex.com/en/referral/fdfc0e', hasReferral: true }, lighter: { url: 'https://app.lighter.xyz/?referral=68151432', hasReferral: true }, } import type { TraderConfigData } from '../types' // 表单内部状态类型 interface FormState { trader_id?: string trader_name: string ai_model: string exchange_id: string strategy_id: string is_cross_margin: boolean show_in_competition: boolean scan_interval_minutes: number initial_balance?: number } interface TraderConfigModalProps { isOpen: boolean onClose: () => void traderData?: TraderConfigData | null isEditMode?: boolean availableModels?: AIModel[] availableExchanges?: Exchange[] onSave?: (data: CreateTraderRequest) => Promise } export function TraderConfigModal({ isOpen, onClose, traderData, isEditMode = false, availableModels = [], availableExchanges = [], onSave, }: TraderConfigModalProps) { const { language } = useLanguage() const [formData, setFormData] = useState({ trader_name: '', ai_model: '', exchange_id: '', strategy_id: '', is_cross_margin: true, show_in_competition: true, scan_interval_minutes: 3, }) const [isSaving, setIsSaving] = useState(false) const [strategies, setStrategies] = useState([]) const [isFetchingBalance, setIsFetchingBalance] = useState(false) const [balanceFetchError, setBalanceFetchError] = useState('') // 获取用户的策略列表 useEffect(() => { const fetchStrategies = async () => { try { const result = await httpClient.get<{ strategies: Strategy[] }>('/api/strategies') if (result.success && result.data?.strategies) { const strategyList = result.data.strategies setStrategies(strategyList) // 如果没有选择策略,默认选中激活的策略 if (!formData.strategy_id && !isEditMode) { const activeStrategy = strategyList.find(s => s.is_active) if (activeStrategy) { setFormData(prev => ({ ...prev, strategy_id: activeStrategy.id })) } else if (strategyList.length > 0) { setFormData(prev => ({ ...prev, strategy_id: strategyList[0].id })) } } } } catch (error) { console.error('Failed to fetch strategies:', error) } } if (isOpen) { fetchStrategies() } }, [isOpen]) useEffect(() => { if (traderData) { setFormData({ ...traderData, strategy_id: traderData.strategy_id || '', }) } else if (!isEditMode) { setFormData({ trader_name: '', ai_model: availableModels[0]?.id || '', exchange_id: availableExchanges[0]?.id || '', strategy_id: '', is_cross_margin: true, show_in_competition: true, scan_interval_minutes: 3, }) } }, [traderData, isEditMode, availableModels, availableExchanges]) if (!isOpen) return null const handleInputChange = (field: keyof FormState, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })) } const handleFetchCurrentBalance = async () => { if (!isEditMode || !traderData?.trader_id) { setBalanceFetchError(t('fetchBalanceEditModeOnly', language)) return } setIsFetchingBalance(true) setBalanceFetchError('') try { const result = await httpClient.get<{ total_equity?: number balance?: number }>(`/api/account?trader_id=${traderData.trader_id}`) if (result.success && result.data) { const currentBalance = result.data.total_equity || result.data.balance || 0 setFormData((prev) => ({ ...prev, initial_balance: currentBalance })) toast.success(t('balanceFetched', language)) } else { throw new Error(result.message || t('balanceFetchFailed', language)) } } catch (error) { console.error(t('balanceFetchFailed', language) + ':', error) setBalanceFetchError(t('balanceFetchNetworkError', language)) } finally { setIsFetchingBalance(false) } } const handleSave = async () => { if (!onSave) return setIsSaving(true) try { const saveData: CreateTraderRequest = { name: formData.trader_name, ai_model_id: formData.ai_model, exchange_id: formData.exchange_id, strategy_id: formData.strategy_id, is_cross_margin: formData.is_cross_margin, show_in_competition: formData.show_in_competition, scan_interval_minutes: formData.scan_interval_minutes, } // 只在编辑模式时包含initial_balance if (isEditMode && formData.initial_balance !== undefined) { saveData.initial_balance = formData.initial_balance } await toast.promise(onSave(saveData), { loading: t('saving', language), success: t('saveSuccess', language), error: t('saveFailed', language), }) onClose() } catch (error) { console.error(t('saveFailed', language) + ':', error) } finally { setIsSaving(false) } } const selectedStrategy = strategies.find(s => s.id === formData.strategy_id) return (
e.stopPropagation()} > {/* Header */}
{isEditMode ? ( ) : ( )}

{isEditMode ? t('editTrader', language) : t('createTrader', language)}

{isEditMode ? t('editTraderConfig', language) : t('selectStrategyAndConfigParams', language)}

{/* Content */}
{/* Basic Info */}

1 {t('basicConfig', language)}

handleInputChange('trader_name', e.target.value) } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" placeholder={t('enterTraderNamePlaceholder', language)} />
{/* Exchange Registration Link */} {formData.exchange_id && (() => { // Find the selected exchange to get its type const selectedExchange = availableExchanges.find(e => e.id === formData.exchange_id) const exchangeType = selectedExchange?.exchange_type?.toLowerCase() || '' const regLink = EXCHANGE_REGISTRATION_LINKS[exchangeType] if (!regLink) return null return ( {t('noExchangeAccount', language)} {regLink.hasReferral && ( {t('discount', language)} )} ) })()}
{/* Strategy Selection */}

2 {t('selectTradingStrategy', language)}

{strategies.length === 0 && (

{t('noStrategyHint', language)}

)}
{/* Strategy Preview */} {selectedStrategy && (
{t('strategyDetails', language)} {selectedStrategy.is_active && ( {t('activating', language)} )}

{selectedStrategy.description || (language === 'zh' ? '无描述' : 'No description')}

{t('coinSource', language)}: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' : selectedStrategy.config.coin_source.source_type === 'ai500' ? 'AI500' : selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
{t('marginLimit', language)}: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
)}
{/* Trading Parameters */}

3 {t('tradingParams', language)}

{ const parsedValue = Number(e.target.value) const safeValue = Number.isFinite(parsedValue) ? Math.max(3, parsedValue) : 3 handleInputChange('scan_interval_minutes', safeValue) }} className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="3" max="60" step="1" />

{t('scanIntervalRecommend', language)}

{/* Competition visibility */}

{t('hiddenInCompetition', language)}

{/* Initial Balance (Edit mode only) */} {isEditMode && (
handleInputChange( 'initial_balance', Number(e.target.value) ) } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="100" step="0.01" />

{t('balanceUpdateHint', language)}

{balanceFetchError && (

{balanceFetchError}

)}
)} {/* Create mode info */} {!isEditMode && (
{t('autoFetchBalanceInfo', language)}
)}
{/* Footer */}
{onSave && ( )}
) }