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://lighter.xyz', hasReferral: false }, } 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('只有在编辑模式下才能获取当前余额') 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('已获取当前余额') } else { throw new Error(result.message || '获取余额失败') } } catch (error) { console.error('获取余额失败:', error) setBalanceFetchError('获取余额失败,请检查网络连接') } 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 || undefined, 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: '正在保存…', success: '保存成功', error: '保存失败', }) onClose() } catch (error) { console.error('保存失败:', error) } finally { setIsSaving(false) } } const selectedStrategy = strategies.find(s => s.id === formData.strategy_id) return (
e.stopPropagation()} > {/* Header */}
{isEditMode ? ( ) : ( )}

{isEditMode ? '修改交易员' : '创建交易员'}

{isEditMode ? '修改交易员配置' : '选择策略并配置基础参数'}

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

1 基础配置

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="请输入交易员名称" />
{/* 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 ( 还没有交易所账号?点击注册 {regLink.hasReferral && ( 折扣优惠 )} ) })()}
{/* Strategy Selection */}

2 选择交易策略

{strategies.length === 0 && (

暂无策略,请先在策略工作室创建策略

)}
{/* Strategy Preview */} {selectedStrategy && (
策略详情 {selectedStrategy.is_active && ( 激活中 )}

{selectedStrategy.description || '无描述'}

币种来源: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' : selectedStrategy.config.coin_source.source_type === 'coinpool' ? 'Coin Pool' : selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
保证金上限: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
)}
{/* Trading Parameters */}

3 交易参数

{ 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 */}

隐藏后将不在竞技场页面显示此交易员

{/* 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" />

用于手动更新初始余额基准(例如充值/提现后)

{balanceFetchError && (

{balanceFetchError}

)}
)} {/* Create mode info */} {!isEditMode && (
系统将自动获取您的账户净值作为初始余额
)}
{/* Footer */}
{onSave && ( )}
) }