import React, { useEffect, useState, useRef } from 'react' import { Modal, Form, Button, message, Radio, InputNumber, Divider, Spin, Select, Input, Space, Switch, Tag, InputRef, Card, Row, Col, Statistic } from 'antd' import { SaveOutlined } from '@ant-design/icons' import { apiService } from '../../services/api' import type { CopyTrading, CopyTradingUpdateRequest } from '../../types' import { useTranslation } from 'react-i18next' import { formatUSDC } from '../../utils' const { Option } = Select interface EditModalProps { open: boolean onClose: () => void copyTradingId: string onSuccess?: () => void } const EditModal: React.FC = ({ open, onClose, copyTradingId, onSuccess }) => { const { t } = useTranslation() const [form] = Form.useForm() const [loading, setLoading] = useState(false) const [fetching, setFetching] = useState(true) const [copyTrading, setCopyTrading] = useState(null) const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO') const [originalEnabled, setOriginalEnabled] = useState(true) 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) useEffect(() => { if (open && copyTradingId) { fetchCopyTrading(parseInt(copyTradingId)) } }, [open, copyTradingId]) const fetchCopyTrading = async (copyTradingId: number) => { setFetching(true) try { const response = await apiService.copyTrading.list({}) if (response.data.code === 0 && response.data.data) { const found = response.data.data.list.find((ct: CopyTrading) => ct.id === copyTradingId) if (found) { setCopyTrading(found) setCopyMode(found.copyMode) setOriginalEnabled(found.enabled) // 解析市场截止时间(毫秒转换为小时或天) if (found.maxMarketEndDate) { const hours = found.maxMarketEndDate / (60 * 60 * 1000) if (hours >= 24 && Number.isInteger(hours / 24)) { // 大于等于24小时且是24的整数倍,使用天作为单位 setMaxMarketEndDateUnit('DAY') setMaxMarketEndDateValue(hours / 24) } else { // 使用小时作为单位 setMaxMarketEndDateUnit('HOUR') setMaxMarketEndDateValue(hours) } } else { setMaxMarketEndDateValue(undefined) setMaxMarketEndDateUnit('HOUR') } form.setFieldsValue({ accountId: found.accountId, leaderId: found.leaderId, copyMode: found.copyMode, copyRatio: found.copyRatio ? parseFloat(found.copyRatio) * 100 : 100, fixedAmount: found.fixedAmount ? parseFloat(found.fixedAmount) : undefined, maxOrderSize: found.maxOrderSize ? parseFloat(found.maxOrderSize) : undefined, minOrderSize: found.minOrderSize ? parseFloat(found.minOrderSize) : undefined, maxDailyLoss: found.maxDailyLoss ? parseFloat(found.maxDailyLoss) : undefined, maxDailyOrders: found.maxDailyOrders, priceTolerance: found.priceTolerance ? parseFloat(found.priceTolerance) : undefined, delaySeconds: found.delaySeconds, pollIntervalSeconds: found.pollIntervalSeconds, useWebSocket: found.useWebSocket, websocketReconnectInterval: found.websocketReconnectInterval, websocketMaxRetries: found.websocketMaxRetries, supportSell: found.supportSell, minOrderDepth: found.minOrderDepth ? parseFloat(found.minOrderDepth) : undefined, maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined, minPrice: found.minPrice ? parseFloat(found.minPrice) : undefined, maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined, maxPositionValue: found.maxPositionValue ? parseFloat(found.maxPositionValue) : undefined, keywordFilterMode: found.keywordFilterMode || 'DISABLED', configName: found.configName || '', pushFailedOrders: found.pushFailedOrders ?? false, pushFilteredOrders: found.pushFilteredOrders ?? false }) // 设置关键字列表 setKeywords(found.keywords || []) // 获取 Leader 资产信息 fetchLeaderAssetInfo(found.leaderId) } else { message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在') onClose() } } else { message.error(response.data.msg || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败') onClose() } } catch (error: any) { message.error(error.message || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败') onClose() } finally { setFetching(false) } } const handleCopyModeChange = (mode: 'RATIO' | 'FIXED') => { setCopyMode(mode) } // 获取 Leader 资产信息 const fetchLeaderAssetInfo = async (leaderId: number) => { 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 handleAddKeyword = (e?: React.KeyboardEvent) => { let inputValue = '' if (e) { const target = e.target as HTMLInputElement inputValue = target.value.trim() } else if (keywordInputRef.current) { inputValue = keywordInputRef.current.input?.value?.trim() || '' } if (!inputValue) { return } if (keywords.includes(inputValue)) { message.warning(t('copyTradingEdit.keywordExists') || 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('固定金额必须 >= 1') return } } if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && Number(values.minOrderSize) < 1) { message.error('最小金额必须 >= 1') return } if (!copyTradingId) { message.error('配置ID不存在') return } // 计算市场截止时间(毫秒) // 如果用户清空了,传 -1 表示要清空(后端会识别并设置为 null) let maxMarketEndDate: number | undefined if (maxMarketEndDateValue !== undefined && maxMarketEndDateValue !== null && maxMarketEndDateValue > 0) { const multiplier = maxMarketEndDateUnit === 'HOUR' ? 60 * 60 * 1000 // 小时转毫秒 : 24 * 60 * 60 * 1000 // 天转毫秒 maxMarketEndDate = maxMarketEndDateValue * multiplier } else { // 如果值为 null/undefined/0/负数,传 -1 表示要清空 // 这样无论之前是否有值,清空后都会设置为 null maxMarketEndDate = -1 } setLoading(true) try { const request: CopyTradingUpdateRequest = { copyTradingId: parseInt(copyTradingId), enabled: originalEnabled, copyMode: values.copyMode, 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, // 对于可选字段,始终发送(即使为空也发送空字符串,让后端知道要清空) minOrderDepth: values.minOrderDepth != null ? values.minOrderDepth.toString() : '', maxSpread: values.maxSpread != null ? values.maxSpread.toString() : '', minPrice: values.minPrice != null ? values.minPrice.toString() : '', maxPrice: values.maxPrice != null ? values.maxPrice.toString() : '', maxPositionValue: values.maxPositionValue != null ? values.maxPositionValue.toString() : '', keywordFilterMode: values.keywordFilterMode || 'DISABLED', keywords: (values.keywordFilterMode === 'WHITELIST' || values.keywordFilterMode === 'BLACKLIST') ? keywords : undefined, configName: values.configName?.trim() || undefined, pushFailedOrders: values.pushFailedOrders, pushFilteredOrders: values.pushFilteredOrders, maxMarketEndDate } const response = await apiService.copyTrading.update(request) if (response.data.code === 0) { message.success(t('copyTradingEdit.saveSuccess') || '更新跟单配置成功') onClose() if (onSuccess) { onSuccess() } } else { message.error(response.data.msg || t('copyTradingEdit.saveFailed') || '更新跟单配置失败') } } catch (error: any) { message.error(error.message || t('copyTradingEdit.saveFailed') || '更新跟单配置失败') } finally { setLoading(false) } } return ( {fetching ? (
) : !copyTrading ? (

{t('copyTradingEdit.fetchFailed') || '跟单配置不存在'}

) : (
{/* Leader 资产信息 */} {t('copyTradingAdd.leaderAssetInfo') || 'Leader 资产信息'} } size="small" style={{ marginBottom: '16px', backgroundColor: '#f5f5f5', border: '1px solid #d9d9d9' }} > {loadingAssetInfo ? (
{t('copyTradingAdd.loadingAssetInfo') || '加载资产信息中...'}
) : leaderAssetInfo ? ( formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> ) : null}
{t('copyTradingEdit.basicConfig') || '基础配置'} handleCopyModeChange(e.target.value)}> {t('copyTradingEdit.ratioMode') || '比例模式'} {t('copyTradingEdit.fixedAmountMode') || '固定金额模式'} {copyMode === 'RATIO' && ( { console.log('[EditModal copyRatio parser] 输入值:', value, '类型:', typeof value) // 移除 % 符号和其他非数字字符(保留小数点和负号) const cleaned = (value || '').toString().replace(/%/g, '').trim() console.log('[EditModal copyRatio parser] 清理后:', cleaned) const parsed = parseFloat(cleaned) || 0 console.log('[EditModal copyRatio parser] 解析后:', parsed) if (parsed > 10000) { console.log('[EditModal copyRatio parser] 超过最大值,返回 10000') return 10000 } if (parsed < 0.01) { console.log('[EditModal copyRatio parser] 小于最小值,返回 0.01') return 0.01 } console.log('[EditModal copyRatio parser] 返回:', parsed) return parsed }} formatter={(value) => { console.log('[EditModal copyRatio formatter] 输入值:', value, '类型:', typeof value) if (!value && value !== 0) { console.log('[EditModal copyRatio formatter] 空值,返回空字符串') return '' } const num = parseFloat(value.toString()) console.log('[EditModal copyRatio formatter] 解析后:', num) if (isNaN(num)) { console.log('[EditModal copyRatio formatter] NaN,返回空字符串') return '' } if (num > 10000) { console.log('[EditModal copyRatio formatter] 超过最大值,返回 10000') return '10000' } const result = num.toString().replace(/\.0+$/, '') console.log('[EditModal copyRatio formatter] 格式化后返回:', result) return result }} /> )} {copyMode === 'FIXED' && ( { if (value !== undefined && value !== null && value !== '') { const amount = Number(value) if (isNaN(amount)) { return Promise.reject(new Error(t('copyTradingEdit.invalidNumber') || '请输入有效的数字')) } if (amount < 1) { return Promise.reject(new Error(t('copyTradingEdit.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('copyTradingEdit.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('copyTradingEdit.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('copyTradingEdit.positionLimitFilter') || '最大仓位限制'} { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> {/* 关键字过滤 */} {t('copyTradingEdit.keywordFilter') || t('copyTradingAdd.keywordFilter') || '关键字过滤'} {t('copyTradingEdit.disabled') || t('copyTradingAdd.disabled') || '不启用'} {t('copyTradingEdit.whitelist') || t('copyTradingAdd.whitelist') || '白名单'} {t('copyTradingEdit.blacklist') || 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('copyTradingEdit.whitelistTooltip') || t('copyTradingAdd.whitelistTooltip') || '💡 白名单模式:只跟单包含上述任意关键字的市场标题') : (t('copyTradingEdit.blacklistTooltip') || t('copyTradingAdd.blacklistTooltip') || '💡 黑名单模式:不跟单包含上述任意关键字的市场标题') }
) }}
{/* 市场截止时间限制 */} {t('copyTradingEdit.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('copyTradingEdit.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('copyTradingEdit.maxMarketEndDateNote') || '💡 说明:不填写表示不启用此限制'}
{t('copyTradingEdit.advancedSettings') || '高级设置'}
)}
) } export default EditModal