import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Table, Button, Space, Tag, Popconfirm, message, Input, Modal, Form, Radio, InputNumber, Switch, Divider, Spin } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { apiService } from '../services/api' import type { CopyTradingTemplate } from '../types' import { useMediaQuery } from 'react-responsive' import { formatUSDC } from '../utils' const { Search } = Input const TemplateList: React.FC = () => { const { t, i18n } = useTranslation() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(false) const [searchText, setSearchText] = useState('') const [copyModalVisible, setCopyModalVisible] = useState(false) const [copyForm] = Form.useForm() const [copyLoading, setCopyLoading] = useState(false) const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO') const [_sourceTemplate, setSourceTemplate] = useState(null) // 用于跟踪复制的源模板 useEffect(() => { fetchTemplates() }, []) const fetchTemplates = async () => { setLoading(true) try { const response = await apiService.templates.list() if (response.data.code === 0 && response.data.data) { setTemplates(response.data.data.list || []) } else { message.error(response.data.msg || t('templateList.fetchFailed') || '获取模板列表失败') } } catch (error: any) { message.error(error.message || t('templateList.fetchFailed') || '获取模板列表失败') } finally { setLoading(false) } } const handleDelete = async (templateId: number) => { try { const response = await apiService.templates.delete({ templateId }) if (response.data.code === 0) { message.success(t('templateList.deleteSuccess') || '删除模板成功') fetchTemplates() } else { message.error(response.data.msg || t('templateList.deleteFailed') || '删除模板失败') } } catch (error: any) { message.error(error.message || t('templateList.deleteFailed') || '删除模板失败') } } const handleCopy = (template: CopyTradingTemplate) => { setSourceTemplate(template) setCopyMode(template.copyMode) // 填充表单数据 copyForm.setFieldsValue({ templateName: `${template.templateName}-${t('templateList.copySuffix') || '副本'}`, 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: parseFloat(template.priceTolerance), 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 }) setCopyModalVisible(true) } const handleCopySubmit = async (values: any) => { // 前端校验:如果填写了 minOrderSize,必须 >= 1 if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && values.minOrderSize !== '' && Number(values.minOrderSize) < 1) { message.error(t('templateList.minAmountError') || '最小金额必须 >= 1') return } // 前端校验:固定金额模式下,fixedAmount 必填且必须 >= 1 if (values.copyMode === 'FIXED') { const fixedAmount = values.fixedAmount if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') { message.error(t('templateList.fixedAmountRequired') || '请输入固定跟单金额') return } const amount = Number(fixedAmount) if (isNaN(amount)) { message.error(t('templateList.invalidNumber') || '请输入有效的数字') return } if (amount < 1) { message.error(t('templateList.fixedAmountError') || '固定金额必须 >= 1,请重新输入') return } } setCopyLoading(true) try { const response = await apiService.templates.create({ templateName: values.templateName, copyMode: values.copyMode || 'RATIO', // 将百分比转换为小数:100% -> 1.0 copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined, fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount?.toString() : undefined, maxOrderSize: values.copyMode === 'RATIO' ? values.maxOrderSize?.toString() : undefined, minOrderSize: values.copyMode === 'RATIO' ? values.minOrderSize?.toString() : undefined, maxDailyOrders: values.maxDailyOrders, priceTolerance: values.priceTolerance?.toString(), supportSell: values.supportSell !== false, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), minPrice: values.minPrice?.toString(), maxPrice: values.maxPrice?.toString() }) if (response.data.code === 0) { message.success(t('templateList.copySuccess') || '复制模板成功') setCopyModalVisible(false) copyForm.resetFields() fetchTemplates() } else { message.error(response.data.msg || t('templateList.copyFailed') || '复制模板失败') } } catch (error: any) { message.error(error.message || t('templateList.copyFailed') || '复制模板失败') } finally { setCopyLoading(false) } } const handleCopyCancel = () => { setCopyModalVisible(false) copyForm.resetFields() setSourceTemplate(null) } const filteredTemplates = templates.filter(template => template.templateName.toLowerCase().includes(searchText.toLowerCase()) ) const columns = [ { title: t('templateList.templateName') || '模板名称', dataIndex: 'templateName', key: 'templateName', render: (text: string) => {text} }, { title: t('templateList.copyMode') || '跟单模式', dataIndex: 'copyMode', key: 'copyMode', render: (mode: string) => ( {mode === 'RATIO' ? t('templateList.ratio') || '比例' : t('templateList.fixedAmount') || '固定金额'} ) }, { title: t('templateList.copyConfig') || '跟单配置', key: 'copyConfig', render: (_: any, record: CopyTradingTemplate) => { if (record.copyMode === 'RATIO') { return `${t('templateList.ratio') || '比例'} ${record.copyRatio}x` } else if (record.copyMode === 'FIXED' && record.fixedAmount) { return `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(record.fixedAmount)} USDC` } return '-' } }, { title: t('templateList.supportSell') || '跟单卖出', dataIndex: 'supportSell', key: 'supportSell', render: (support: boolean) => ( {support ? t('common.yes') || '是' : t('common.no') || '否'} ) }, { title: t('common.createdAt') || '创建时间', dataIndex: 'createdAt', key: 'createdAt', render: (timestamp: number) => { const date = new Date(timestamp) return date.toLocaleString(i18n.language || 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }) }, sorter: (a: CopyTradingTemplate, b: CopyTradingTemplate) => a.createdAt - b.createdAt, defaultSortOrder: 'descend' as const }, { title: t('common.actions') || '操作', key: 'action', width: isMobile ? 120 : 200, render: (_: any, record: CopyTradingTemplate) => ( handleDelete(record.id)} okText={t('common.confirm') || '确定'} cancelText={t('common.cancel') || '取消'} > ) } ] return (

{t('templateList.title') || '跟单模板管理'}

setSearchText(e.target.value)} />
{isMobile ? ( // 移动端卡片布局
{loading ? (
) : filteredTemplates.length === 0 ? (
{t('templateList.noData') || '暂无模板数据'}
) : (
{filteredTemplates.map((template) => { const date = new Date(template.createdAt) const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) return ( {/* 模板名称和模式 */}
{template.templateName}
{template.copyMode === 'RATIO' ? (t('templateList.ratioMode') || '比例模式') : (t('templateList.fixedAmountMode') || '固定金额模式')} {template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
{/* 跟单配置 */}
{t('templateList.copyConfig') || '跟单配置'}
{template.copyMode === 'RATIO' ? `${t('templateList.ratio') || '比例'} ${template.copyRatio}x` : template.fixedAmount ? `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(template.fixedAmount)} USDC` : '-' }
{/* 其他配置信息 */} {template.copyMode === 'RATIO' && (
{t('templateList.amountLimit') || '金额限制'}
{template.maxOrderSize && ( {t('templateList.max') || '最大'}: {formatUSDC(template.maxOrderSize)} USDC )} {template.maxOrderSize && template.minOrderSize && | } {template.minOrderSize && ( {t('templateList.min') || '最小'}: {formatUSDC(template.minOrderSize)} USDC )} {!template.maxOrderSize && !template.minOrderSize && {t('templateList.notSet') || '未设置'}}
)}
{t('templateList.otherConfig') || '其他配置'}
{t('templateList.maxDailyOrders') || '每日最大订单'}: {template.maxDailyOrders} | {t('templateList.priceTolerance') || '价格容忍度'}: {template.priceTolerance}%
{/* 创建时间 */}
{t('common.createdAt') || '创建时间'}: {formattedDate}
{/* 操作按钮 */}
handleDelete(template.id)} okText={t('common.confirm') || '确定'} cancelText={t('common.cancel') || '取消'} >
) })}
)}
) : ( // 桌面端表格布局 `共 ${total} 条` }} /> )}
比例模式 固定金额模式 {copyMode === 'RATIO' && ( { const parsed = parseFloat(value || '0') if (parsed > 10000) return 10000 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('请输入有效的数字')) } if (amount < 1) { return Promise.reject(new Error('固定金额必须 >= 1,请重新输入')) } } return Promise.resolve() } } ]} > { 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+$/, '') }} /> { if (value === undefined || value === null || value === '') { return Promise.resolve() } if (typeof value === 'number' && value < 1) { return Promise.reject(new Error('最小金额必须 >= 1')) } return Promise.resolve() } } ]} > { 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+$/, '') }} /> - { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' return num.toString().replace(/\.0+$/, '') }} /> {({ getFieldsError }) => { const errors = getFieldsError() const hasErrors = errors.some(({ errors }) => errors && errors.length > 0) return ( ) }}
) } export default TemplateList