import React, { useEffect, useState, useRef } from 'react' import { Modal, Form, Button, Switch, message, Space, Radio, InputNumber, Table, Select, Divider, Input, Tag, InputRef, Card, Row, Col, Statistic, Spin } from 'antd' import { SaveOutlined, FileTextOutlined, PlusOutlined } from '@ant-design/icons' import { apiService } from '../../services/api' import { useAccountStore } from '../../store/accountStore' import type { Leader, CopyTradingTemplate, CopyTradingCreateRequest } from '../../types' import { formatUSDC } from '../../utils' import { useTranslation } from 'react-i18next' import { useMediaQuery } from 'react-responsive' import AccountImportForm from '../../components/AccountImportForm' import LeaderAddForm from '../../components/LeaderAddForm' import LeaderSelect from '../../components/LeaderSelect' const { Option } = Select interface AddModalProps { open: boolean onClose: () => void onSuccess?: () => void preFilledConfig?: { leaderId?: number copyMode?: 'RATIO' | 'FIXED' copyRatio?: number fixedAmount?: string maxOrderSize?: number minOrderSize?: number maxDailyLoss?: number maxDailyOrders?: number supportSell?: boolean keywordFilterMode?: string keywords?: string[] maxPositionValue?: number configName?: string } } const AddModal: React.FC = ({ open, onClose, onSuccess, preFilledConfig }) => { const { t } = useTranslation() const isMobile = useMediaQuery({ maxWidth: 768 }) const { accounts, fetchAccounts } = useAccountStore() const [form] = Form.useForm() const [loading, setLoading] = useState(false) const [leaders, setLeaders] = useState([]) const [templates, setTemplates] = useState([]) const [templateModalVisible, setTemplateModalVisible] = useState(false) const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO') const [keywords, setKeywords] = useState([]) const keywordInputRef = useRef(null) const [maxMarketEndDateValue, setMaxMarketEndDateValue] = useState() const [maxMarketEndDateUnit, setMaxMarketEndDateUnit] = useState<'HOUR' | 'DAY'>('HOUR') const [leaderAssetInfo, setLeaderAssetInfo] = useState<{ total: string; available: string; position: string } | null>(null) const [loadingAssetInfo, setLoadingAssetInfo] = useState(false) // 导入账户modal相关状态 const [accountImportModalVisible, setAccountImportModalVisible] = useState(false) const [accountImportForm] = Form.useForm() // 添加leader modal相关状态 const [leaderAddModalVisible, setLeaderAddModalVisible] = useState(false) const [leaderAddForm] = Form.useForm() // 生成默认配置名 const generateDefaultConfigName = (): string => { const now = new Date() const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-') const timeStr = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) return `跟单配置-${dateStr}-${timeStr}` } // 获取 Leader 资产信息 const fetchLeaderAssetInfo = async (leaderId: number) => { if (!leaderId) return setLoadingAssetInfo(true) setLeaderAssetInfo(null) try { const response = await apiService.leaders.balance({ leaderId }) if (response.data.code === 0 && response.data.data) { const balance = response.data.data setLeaderAssetInfo({ total: balance.totalBalance || '0', available: balance.availableBalance || '0', position: balance.positionBalance || '0' }) } else { message.error(response.data.msg || t('copyTradingAdd.fetchAssetInfoFailed') || '获取资产信息失败') } } catch (error: any) { console.error('获取 Leader 资产失败:', error) message.error(error.message || t('copyTradingAdd.fetchAssetInfoFailed') || '获取资产信息失败') } finally { setLoadingAssetInfo(false) } } // 填充预配置数据到表单(复用模板填充逻辑) const fillPreFilledConfig = (config: typeof preFilledConfig) => { console.log('[AddModal] fillPreFilledConfig called with config:', config) if (!config) { console.log('[AddModal] fillPreFilledConfig: config is null/undefined') return } const formValues = { configName: config.configName || generateDefaultConfigName(), leaderId: config.leaderId, copyMode: config.copyMode || 'RATIO', copyRatio: config.copyRatio, fixedAmount: config.fixedAmount, maxOrderSize: config.maxOrderSize, minOrderSize: config.minOrderSize, maxDailyLoss: config.maxDailyLoss, maxDailyOrders: config.maxDailyOrders, supportSell: config.supportSell, keywordFilterMode: config.keywordFilterMode || 'DISABLED', maxPositionValue: config.maxPositionValue } console.log('[AddModal] fillPreFilledConfig: setting form values:', formValues) form.setFieldsValue(formValues) setCopyMode(config.copyMode || 'RATIO') setKeywords(config.keywords || []) console.log('[AddModal] fillPreFilledConfig: form values set, copyMode:', config.copyMode, 'keywords:', config.keywords) // 自动获取 Leader 资产信息 if (config.leaderId) { console.log('[AddModal] fillPreFilledConfig: fetching leader asset info for leaderId:', config.leaderId) fetchLeaderAssetInfo(config.leaderId) } } // 处理 Modal 打开/关闭 useEffect(() => { console.log('[AddModal] useEffect triggered, open:', open, 'preFilledConfig:', preFilledConfig) if (open) { console.log('[AddModal] Modal opened, fetching accounts, leaders, templates') fetchAccounts() fetchLeaders() fetchTemplates() // 如果有预填充配置,填充表单(延迟执行确保数据已加载) if (preFilledConfig) { console.log('[AddModal] preFilledConfig exists, will fill form after 100ms') // 使用 setTimeout 确保在下一个事件循环执行,此时 Modal 已完全打开 setTimeout(() => { console.log('[AddModal] setTimeout callback executed, calling fillPreFilledConfig') fillPreFilledConfig(preFilledConfig) }, 100) } else { console.log('[AddModal] No preFilledConfig, using default values') // 没有预填充配置时,生成默认配置名 const defaultConfigName = generateDefaultConfigName() form.setFieldsValue({ configName: defaultConfigName, copyMode: 'RATIO', copyRatio: 100, maxOrderSize: 1000, minOrderSize: 1, maxDailyLoss: 10000, maxDailyOrders: 100, supportSell: true, keywordFilterMode: 'DISABLED' }) setCopyMode('RATIO') setKeywords([]) } } else { console.log('[AddModal] Modal closed, resetting form') // 关闭时重置表单 form.resetFields() setKeywords([]) setCopyMode('RATIO') setLeaderAssetInfo(null) } }, [open, preFilledConfig]) const fetchLeaders = async () => { try { const response = await apiService.leaders.list({}) if (response.data.code === 0 && response.data.data) { setLeaders(response.data.data.list || []) } } catch (error: any) { message.error(error.message || t('copyTradingAdd.fetchLeaderFailed') || '获取 Leader 列表失败') } } const fetchTemplates = async () => { try { const response = await apiService.templates.list() if (response.data.code === 0 && response.data.data) { setTemplates(response.data.data.list || []) } } catch (error: any) { message.error(error.message || t('copyTradingAdd.fetchTemplateFailed') || '获取模板列表失败') } } const handleSelectTemplate = (template: CopyTradingTemplate) => { // 填充模板数据到表单(只填充模板中存在的字段) form.setFieldsValue({ copyMode: template.copyMode, copyRatio: template.copyRatio ? parseFloat(template.copyRatio) * 100 : 100, // 转换为百分比显示 fixedAmount: template.fixedAmount ? parseFloat(template.fixedAmount) : undefined, maxOrderSize: template.maxOrderSize ? parseFloat(template.maxOrderSize) : undefined, minOrderSize: template.minOrderSize ? parseFloat(template.minOrderSize) : undefined, maxDailyOrders: template.maxDailyOrders, priceTolerance: template.priceTolerance ? parseFloat(template.priceTolerance) : undefined, supportSell: template.supportSell, minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined, maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined, minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined, maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined, maxPositionValue: (template as any).maxPositionValue ? parseFloat((template as any).maxPositionValue) : undefined, pushFilteredOrders: template.pushFilteredOrders ?? false }) setCopyMode(template.copyMode) setTemplateModalVisible(false) message.success(t('copyTradingAdd.templateFilled') || '模板内容已填充,您可以修改') } const handleCopyModeChange = (mode: 'RATIO' | 'FIXED') => { setCopyMode(mode) } // 处理导入账户成功 const handleAccountImportSuccess = async (accountId: number) => { message.success(t('accountImport.importSuccess')) // 刷新账户列表 await fetchAccounts() // 自动选择新添加的账户 form.setFieldsValue({ accountId }) // 关闭modal并重置表单 setAccountImportModalVisible(false) accountImportForm.resetFields() } // 处理添加leader成功 const handleLeaderAddSuccess = async (leaderId: number) => { message.success(t('leaderAdd.addSuccess') || '添加 Leader 成功') // 刷新leader列表 await fetchLeaders() // 自动选择新添加的leader form.setFieldsValue({ leaderId }) // 关闭modal并重置表单 setLeaderAddModalVisible(false) leaderAddForm.resetFields() } // 添加关键字 const handleAddKeyword = (e?: React.KeyboardEvent) => { let inputValue = '' if (e) { // 从键盘事件获取输入值 const target = e.target as HTMLInputElement inputValue = target.value.trim() } else if (keywordInputRef.current) { // 从输入框 ref 获取值 inputValue = keywordInputRef.current.input?.value?.trim() || '' } if (!inputValue) { return } // 检查是否已存在 if (keywords.includes(inputValue)) { message.warning(t('copyTradingAdd.keywordExists') || '关键字已存在') return } // 添加关键字 const newKeywords = [...keywords, inputValue] setKeywords(newKeywords) // 清空输入框 if (keywordInputRef.current) { keywordInputRef.current.input!.value = '' } } // 删除关键字 const handleRemoveKeyword = (index: number) => { const newKeywords = keywords.filter((_, i) => i !== index) setKeywords(newKeywords) } const handleSubmit = async (values: any) => { // 前端校验 if (values.copyMode === 'FIXED') { if (!values.fixedAmount || Number(values.fixedAmount) < 1) { message.error(t('copyTradingAdd.fixedAmountMin') || '固定金额必须 >= 1') return } } if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && Number(values.minOrderSize) < 1) { message.error(t('copyTradingAdd.minOrderSizeMin') || '最小金额必须 >= 1') return } // 计算市场截止时间(毫秒) let maxMarketEndDate: number | undefined if (maxMarketEndDateValue !== undefined && maxMarketEndDateValue > 0) { const multiplier = maxMarketEndDateUnit === 'HOUR' ? 60 * 60 * 1000 // 小时转毫秒 : 24 * 60 * 60 * 1000 // 天转毫秒 maxMarketEndDate = maxMarketEndDateValue * multiplier } setLoading(true) try { const request: CopyTradingCreateRequest = { accountId: values.accountId, leaderId: values.leaderId, enabled: true, // 默认启用 copyMode: values.copyMode || 'RATIO', copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined, fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount?.toString() : undefined, maxOrderSize: values.maxOrderSize?.toString(), minOrderSize: values.minOrderSize?.toString(), maxDailyLoss: values.maxDailyLoss?.toString(), maxDailyOrders: values.maxDailyOrders, priceTolerance: values.priceTolerance?.toString(), delaySeconds: values.delaySeconds, pollIntervalSeconds: values.pollIntervalSeconds, useWebSocket: values.useWebSocket, websocketReconnectInterval: values.websocketReconnectInterval, websocketMaxRetries: values.websocketMaxRetries, supportSell: values.supportSell !== false, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), minPrice: values.minPrice?.toString(), maxPrice: values.maxPrice?.toString(), maxPositionValue: values.maxPositionValue?.toString(), keywordFilterMode: values.keywordFilterMode || 'DISABLED', keywords: (values.keywordFilterMode === 'WHITELIST' || values.keywordFilterMode === 'BLACKLIST') ? keywords : undefined, configName: values.configName?.trim(), pushFailedOrders: values.pushFailedOrders ?? false, pushFilteredOrders: values.pushFilteredOrders ?? false, maxMarketEndDate } const response = await apiService.copyTrading.create(request) if (response.data.code === 0) { message.success(t('copyTradingAdd.createSuccess') || '创建跟单配置成功') onClose() if (onSuccess) { onSuccess() } } else { message.error(response.data.msg || t('copyTradingAdd.createFailed') || '创建跟单配置失败') } } catch (error: any) { message.error(error.message || t('copyTradingAdd.createFailed') || '创建跟单配置失败') } finally { setLoading(false) } } return ( <>
{/* 基础信息 */} value !== undefined && fetchLeaderAssetInfo(value)} notFoundContent={ leaders.length === 0 ? (
{t('copyTradingAdd.noLeaders') || '暂无 Leader'}
) : null } />
{/* Leader 资产信息 */} {leaderAssetInfo && ( {t('copyTradingAdd.leaderAssetInfo') || 'Leader 资产信息'} } size="small" style={{ marginBottom: '16px', backgroundColor: '#f5f5f5', border: '1px solid #d9d9d9' }} > {loadingAssetInfo ? (
{t('copyTradingAdd.loadingAssetInfo') || '加载资产信息中...'}
) : ( formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> )}
)} {/* 模板填充按钮 */} {/* 跟单金额模式 */} handleCopyModeChange(e.target.value)}> {t('copyTradingAdd.ratioMode') || '比例模式'} {t('copyTradingAdd.fixedAmountMode') || '固定金额模式'} {copyMode === 'RATIO' && ( { const cleaned = (value || '').toString().replace(/%/g, '').trim() const parsed = parseFloat(cleaned) || 0 if (parsed > 10000) return 10000 if (parsed < 0.01) return 0.01 return parsed }} formatter={(value) => { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' if (num > 10000) return '10000' return num.toString().replace(/\.0+$/, '') }} /> )} {copyMode === 'FIXED' && ( { if (value !== undefined && value !== null && value !== '') { const amount = Number(value) if (isNaN(amount)) { return Promise.reject(new Error(t('copyTradingAdd.invalidNumber') || '请输入有效的数字')) } if (amount < 1) { return Promise.reject(new Error(t('copyTradingAdd.fixedAmountMin') || '固定金额必须 >= 1')) } } return Promise.resolve() } } ]} > = 1'} formatter={(value) => { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> )} {copyMode === 'RATIO' && ( <> { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> = 1'} rules={[ { validator: (_, value) => { if (value === undefined || value === null || value === '') { return Promise.resolve() } if (typeof value === 'number' && value < 1) { return Promise.reject(new Error(t('copyTradingAdd.minOrderSizeMin') || '最小金额必须 >= 1')) } return Promise.resolve() } } ]} > = 1(可选)'} formatter={(value) => { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> )} { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> {t('copyTradingAdd.priceRangeFilter') || '价格区间过滤'} { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> - { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> {t('copyTradingAdd.positionLimitFilter') || '最大仓位限制'} { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> {t('copyTradingAdd.keywordFilter') || '关键字过滤'} {t('copyTradingAdd.disabled') || '不启用'} {t('copyTradingAdd.whitelist') || '白名单'} {t('copyTradingAdd.blacklist') || '黑名单'} prevValues.keywordFilterMode !== currentValues.keywordFilterMode }> {({ getFieldValue }) => { const filterMode = getFieldValue('keywordFilterMode') if (filterMode !== 'WHITELIST' && filterMode !== 'BLACKLIST') { return null } return ( <> handleAddKeyword(e)} /> {keywords.length > 0 && (
{keywords.map((keyword, index) => ( handleRemoveKeyword(index)} color={filterMode === 'WHITELIST' ? 'green' : 'red'} > {keyword} ))}
)}
{filterMode === 'WHITELIST' ? (t('copyTradingAdd.whitelistTooltip') || '💡 白名单模式:只跟单包含上述任意关键字的市场标题') : (t('copyTradingAdd.blacklistTooltip') || '💡 黑名单模式:不跟单包含上述任意关键字的市场标题') }
) }}
{/* 市场截止时间限制 */} {t('copyTradingAdd.marketEndDateFilter') || '市场截止时间限制'} { // 允许设置为 null 或 undefined(清空) if (value === null || value === undefined) { setMaxMarketEndDateValue(undefined) } else { const num = Math.floor(value) // 如果值为 0,也设置为 undefined(表示清空) setMaxMarketEndDateValue(num > 0 ? num : undefined) } }} onBlur={(e) => { // 失去焦点时,如果值为 0 或空,设置为 undefined const input = e.target as HTMLInputElement const value = input.value if (!value || value === '0') { setMaxMarketEndDateValue(undefined) } }} style={{ width: '60%' }} placeholder={t('copyTradingAdd.maxMarketEndDatePlaceholder') || '输入时间值(可选)'} parser={(value) => { if (!value) return 0 const num = parseInt(value.replace(/\D/g, ''), 10) return isNaN(num) ? 0 : num }} formatter={(value) => { if (!value && value !== 0) return '' return Math.floor(value).toString() }} />
{t('copyTradingAdd.maxMarketEndDateNote') || '💡 说明:不填写表示不启用此限制'}
{t('copyTradingAdd.advancedSettings') || '高级设置'} {/* 跟单卖出 */} {/* 推送失败订单 */} {/* 推送已过滤订单 */}
{/* 模板选择 Modal */} setTemplateModalVisible(false)} footer={null} width={800} > ({ onClick: () => handleSelectTemplate(record), style: { cursor: 'pointer' } })} columns={[ { title: t('copyTradingAdd.templateName') || '模板名称', dataIndex: 'templateName', key: 'templateName' }, { title: t('copyTradingAdd.copyMode') || '跟单模式', key: 'copyMode', render: (_: any, record: CopyTradingTemplate) => ( {record.copyMode === 'RATIO' ? `${t('copyTradingAdd.ratioMode') || '比例'} ${record.copyRatio}x` : `${t('copyTradingAdd.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC` } ) }, { title: t('copyTradingAdd.supportSell') || '跟单卖出', dataIndex: 'supportSell', key: 'supportSell', render: (supportSell: boolean) => supportSell ? (t('common.yes') || '是') : (t('common.no') || '否') } ]} /> {/* 导入账户 Modal */} { setAccountImportModalVisible(false) accountImportForm.resetFields() }} footer={null} width={isMobile ? '95%' : 640} style={{ top: isMobile ? 20 : 50 }} bodyStyle={{ padding: isMobile ? '16px 20px' : '24px 28px', maxHeight: 'calc(100vh - 140px)', overflow: 'auto' }} destroyOnClose maskClosable closable > { setAccountImportModalVisible(false) accountImportForm.resetFields() }} /> {/* 添加 Leader Modal */} { setLeaderAddModalVisible(false) leaderAddForm.resetFields() }} footer={null} width={isMobile ? '95%' : 600} style={{ top: isMobile ? 20 : 50 }} bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 150px)', overflow: 'auto' }} destroyOnClose maskClosable closable > { setLeaderAddModalVisible(false) leaderAddForm.resetFields() }} showCancelButton={true} /> ) } export default AddModal