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, Row, Col, Divider, Spin } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons' 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 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 || '获取模板列表失败') } } catch (error: any) { message.error(error.message || '获取模板列表失败') } finally { setLoading(false) } } const handleDelete = async (templateId: number) => { try { const response = await apiService.templates.delete({ templateId }) if (response.data.code === 0) { message.success('删除模板成功') fetchTemplates() } else { message.error(response.data.msg || '删除模板失败') } } catch (error: any) { message.error(error.message || '删除模板失败') } } const handleCopy = (template: CopyTradingTemplate) => { setSourceTemplate(template) setCopyMode(template.copyMode) // 填充表单数据 copyForm.setFieldsValue({ templateName: `${template.templateName}-副本`, 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 }) 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('最小金额必须 >= 1') return } // 前端校验:固定金额模式下,fixedAmount 必填且必须 >= 1 if (values.copyMode === 'FIXED') { const fixedAmount = values.fixedAmount if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') { message.error('请输入固定跟单金额') return } const amount = Number(fixedAmount) if (isNaN(amount)) { message.error('请输入有效的数字') return } if (amount < 1) { message.error('固定金额必须 >= 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 }) if (response.data.code === 0) { message.success('复制模板成功') setCopyModalVisible(false) copyForm.resetFields() fetchTemplates() } else { message.error(response.data.msg || '复制模板失败') } } catch (error: any) { message.error(error.message || '复制模板失败') } 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: '模板名称', dataIndex: 'templateName', key: 'templateName', render: (text: string) => {text} }, { title: '跟单模式', dataIndex: 'copyMode', key: 'copyMode', render: (mode: string) => ( {mode === 'RATIO' ? '比例' : '固定金额'} ) }, { title: '跟单配置', key: 'copyConfig', render: (_: any, record: CopyTradingTemplate) => { if (record.copyMode === 'RATIO') { return `比例 ${record.copyRatio}x` } else if (record.copyMode === 'FIXED' && record.fixedAmount) { return `固定 ${formatUSDC(record.fixedAmount)} USDC` } return '-' } }, { title: '跟单卖出', dataIndex: 'supportSell', key: 'supportSell', render: (support: boolean) => ( {support ? '是' : '否'} ) }, { title: '使用次数', dataIndex: 'useCount', key: 'useCount', render: (count: number) => {count} }, { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', render: (timestamp: number) => { const date = new Date(timestamp) return date.toLocaleString('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: '操作', key: 'action', width: isMobile ? 120 : 200, render: (_: any, record: CopyTradingTemplate) => ( handleDelete(record.id)} okText="确定" cancelText="取消" > ) } ] return (

跟单模板管理

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