feat: 添加价格区间过滤功能并优化UI

- 添加价格区间过滤功能(minPrice/maxPrice)
  - 数据库迁移:添加价格区间字段到 copy_trading 和 copy_trading_templates 表
  - 后端:实体类、DTO、服务层支持价格区间配置和过滤
  - 前端:模板和配置页面添加价格区间配置UI
  - 过滤逻辑:在订单创建前检查价格是否在配置区间内

- 优化价格区间文案,使其更易懂
  - 明确说明是 Leader 交易价格
  - 详细说明三种配置方式(区间、只填最低价、只填最高价)

- 补充多语言翻译
  - 为 zh-CN、zh-TW、en 添加价格区间相关翻译
  - 添加价格区间过滤类型翻译
  - 添加通用翻译(prev、next、items、total)

- 修复复制模板功能
  - 补充缺失的过滤条件字段(minOrderDepth、maxSpread、minOrderbookDepth、minPrice、maxPrice)
  - 在复制模板表单中添加所有过滤条件配置项

- 已过滤订单列表优化
  - 添加移动端卡片样式布局
  - 在筛选下拉菜单中添加价格区间过滤类型选项

- 订单页面返回按钮优化
  - 买入订单、卖出订单、匹配关系页面的返回按钮改为返回上一页(navigate(-1))
  - 使用多语言支持返回按钮文本
This commit is contained in:
WrBug
2025-12-05 23:52:49 +08:00
parent 6c13362b17
commit 3369dbb248
26 changed files with 753 additions and 127 deletions
+39 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Modal, Table, Select } from 'antd'
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Modal, Table, Select, Divider, Input } from 'antd'
import { ArrowLeftOutlined, SaveOutlined, FileTextOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useAccountStore } from '../store/accountStore'
@@ -63,7 +63,9 @@ const CopyTradingAdd: React.FC = () => {
supportSell: template.supportSell,
minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined,
maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined,
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined,
minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined,
maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined
})
setCopyMode(template.copyMode)
setTemplateModalVisible(false)
@@ -110,7 +112,9 @@ const CopyTradingAdd: React.FC = () => {
supportSell: values.supportSell !== false,
minOrderDepth: values.minOrderDepth?.toString(),
maxSpread: values.maxSpread?.toString(),
minOrderbookDepth: values.minOrderbookDepth?.toString()
minOrderbookDepth: values.minOrderbookDepth?.toString(),
minPrice: values.minPrice?.toString(),
maxPrice: values.maxPrice?.toString()
}
const response = await apiService.copyTrading.create(request)
@@ -408,6 +412,38 @@ const CopyTradingAdd: React.FC = () => {
/>
</Form.Item>
<Divider>{t('copyTradingAdd.priceRangeFilter') || '价格区间过滤'}</Divider>
<Form.Item
label={t('copyTradingAdd.priceRange') || '价格区间'}
name="priceRange"
tooltip={t('copyTradingAdd.priceRangeTooltip') || '配置价格区间,仅在指定价格区间内的订单才会下单。例如:0.11-0.89 表示区间在0.11和0.89之间;-0.89 表示0.89以下都可以;0.11- 表示0.11以上都可以'}
>
<Input.Group compact style={{ display: 'flex' }}>
<Form.Item name="minPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('copyTradingAdd.minPricePlaceholder') || '最低价(可选)'}
/>
</Form.Item>
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
<Form.Item name="maxPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('copyTradingAdd.maxPricePlaceholder') || '最高价(可选)'}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{/* 跟单卖出 - 表单最底部 */}
<Form.Item
label={t('copyTradingAdd.supportSell') || '跟单卖出'}
+4 -2
View File
@@ -5,11 +5,13 @@ import { LeftOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { formatUSDC } from '../utils'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { BuyOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
const { Option } = Select
const CopyTradingBuyOrdersPage: React.FC = () => {
const { t } = useTranslation()
const { copyTradingId } = useParams<{ copyTradingId: string }>()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -199,8 +201,8 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
<Button icon={<LeftOutlined />} onClick={() => navigate(-1)}>
{t('common.back') || '返回'}
</Button>
<h2 style={{ margin: 0 }}></h2>
</div>
+39 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Divider, Spin, Select } from 'antd'
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Divider, Spin, Select, Input } from 'antd'
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import type { CopyTrading, CopyTradingUpdateRequest } from '../types'
@@ -56,7 +56,9 @@ const CopyTradingEdit: React.FC = () => {
supportSell: found.supportSell,
minOrderDepth: found.minOrderDepth ? parseFloat(found.minOrderDepth) : undefined,
maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined,
minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined
minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined,
minPrice: found.minPrice ? parseFloat(found.minPrice) : undefined,
maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined
})
} else {
message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在')
@@ -118,7 +120,9 @@ const CopyTradingEdit: React.FC = () => {
supportSell: values.supportSell,
minOrderDepth: values.minOrderDepth?.toString(),
maxSpread: values.maxSpread?.toString(),
minOrderbookDepth: values.minOrderbookDepth?.toString()
minOrderbookDepth: values.minOrderbookDepth?.toString(),
minPrice: values.minPrice?.toString(),
maxPrice: values.maxPrice?.toString()
}
const response = await apiService.copyTrading.update(request)
@@ -397,6 +401,38 @@ const CopyTradingEdit: React.FC = () => {
/>
</Form.Item>
<Divider>{t('copyTradingEdit.priceRangeFilter') || '价格区间过滤'}</Divider>
<Form.Item
label={t('copyTradingEdit.priceRange') || '价格区间'}
name="priceRange"
tooltip={t('copyTradingEdit.priceRangeTooltip') || '配置价格区间,仅在指定价格区间内的订单才会下单。例如:0.11-0.89 表示区间在0.11和0.89之间;-0.89 表示0.89以下都可以;0.11- 表示0.11以上都可以'}
>
<Input.Group compact style={{ display: 'flex' }}>
<Form.Item name="minPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('copyTradingEdit.minPricePlaceholder') || '最低价(可选)'}
/>
</Form.Item>
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
<Form.Item name="maxPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('copyTradingEdit.maxPricePlaceholder') || '最高价(可选)'}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{/* 跟单卖出 - 表单最底部 */}
<Form.Item
label={t('copyTradingEdit.supportSell') || '跟单卖出'}
+24 -22
View File
@@ -264,12 +264,6 @@ const CopyTradingList: React.FC = () => {
{
type: 'divider'
},
{
key: 'statistics',
label: t('copyTradingList.viewStatistics') || '查看统计',
icon: <BarChartOutlined />,
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
},
{
key: 'buyOrders',
label: t('copyTradingList.buyOrders') || '买入订单',
@@ -290,7 +284,7 @@ const CopyTradingList: React.FC = () => {
},
{
key: 'filteredOrders',
label: t('copyTradingList.filteredOrders') || '过滤订单',
label: t('copyTradingList.filteredOrders') || '过滤订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`)
},
@@ -550,39 +544,47 @@ const CopyTradingList: React.FC = () => {
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<Button
type="primary"
size="small"
icon={<EditOutlined />}
onClick={() => navigate(`/copy-trading/edit/${record.id}`)}
style={{ flex: 1, minWidth: '80px' }}
>
{t('common.edit') || '编辑'}
</Button>
<Button
size="small"
icon={<BarChartOutlined />}
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
style={{ flex: 1, minWidth: '80px' }}
>
{t('copyTradingList.statistics') || '统计'}
</Button>
<Dropdown
menu={{
items: [
{
key: 'statistics',
label: '查看统计',
icon: <BarChartOutlined />,
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
},
{
key: 'buyOrders',
label: '买入订单',
label: t('copyTradingList.buyOrders') || '买入订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
},
{
key: 'sellOrders',
label: '卖出订单',
label: t('copyTradingList.sellOrders') || '卖出订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
},
{
key: 'matchedOrders',
label: '匹配关系',
label: t('copyTradingList.matchedOrders') || '匹配关系',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
},
{
key: 'filteredOrders',
label: t('copyTradingList.filteredOrders') || '已过滤订单',
icon: <UnorderedListOutlined />,
onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`)
}
]
}}
@@ -593,14 +595,14 @@ const CopyTradingList: React.FC = () => {
icon={<UnorderedListOutlined />}
style={{ flex: 1, minWidth: '80px' }}
>
{t('copyTradingList.orders') || '订单'}
</Button>
</Dropdown>
<Popconfirm
title="确定要删除这个跟单关系吗?"
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
<Button
danger
@@ -608,7 +610,7 @@ const CopyTradingList: React.FC = () => {
icon={<DeleteOutlined />}
style={{ flex: 1, minWidth: '80px' }}
>
{t('common.delete') || '删除'}
</Button>
</Popconfirm>
</div>
@@ -5,9 +5,11 @@ import { LeftOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { formatUSDC } from '../utils'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
const CopyTradingMatchedOrdersPage: React.FC = () => {
const { t } = useTranslation()
const { copyTradingId } = useParams<{ copyTradingId: string }>()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -153,8 +155,8 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
<Button icon={<LeftOutlined />} onClick={() => navigate(-1)}>
{t('common.back') || '返回'}
</Button>
<h2 style={{ margin: 0 }}></h2>
</div>
+4 -2
View File
@@ -5,11 +5,13 @@ import { LeftOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { formatUSDC } from '../utils'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
const { Option } = Select
const CopyTradingSellOrdersPage: React.FC = () => {
const { t } = useTranslation()
const { copyTradingId } = useParams<{ copyTradingId: string }>()
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -184,8 +186,8 @@ const CopyTradingSellOrdersPage: React.FC = () => {
<Card>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
<Button icon={<LeftOutlined />} onClick={() => navigate(-1)}>
{t('common.back') || '返回'}
</Button>
<h2 style={{ margin: 0 }}></h2>
</div>
+4 -19
View File
@@ -110,7 +110,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
</div>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<Col xs={24} sm={12} md={8}>
<div>
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}>Leader </div>
<div style={{ fontSize: 16, fontWeight: 500 }}>
@@ -118,15 +118,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
</div>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div>
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}></div>
<div style={{ fontSize: 16, fontWeight: 500 }}>
{statistics.templateName || `模板 ${statistics.templateId}`}
</div>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<Col xs={24} sm={12} md={8}>
<div>
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}></div>
<div>
@@ -203,21 +195,14 @@ const CopyTradingStatisticsPage: React.FC = () => {
{/* 持仓统计卡片 */}
<Card title="持仓统计" style={{ marginBottom: 16 }}>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8}>
<Col xs={24} sm={12} md={12}>
<Statistic
title="当前持仓数量"
value={formatUSDC(statistics.currentPositionQuantity)}
suffix=""
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title="当前持仓价值"
value={formatUSDC(statistics.currentPositionValue)}
suffix="USDC"
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Col xs={24} sm={12} md={12}>
<Statistic
title="平均买入价格"
value={formatUSDC(statistics.avgBuyPrice)}
+178 -17
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Card, Table, Button, Tag, Select, Space, message } from 'antd'
import { Card, Table, Button, Tag, Select, Space, message, Divider, Spin } from 'antd'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
@@ -64,6 +64,7 @@ const FilteredOrdersList: React.FC = () => {
'MARKET_STATUS': { color: 'blue', label: t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易' },
'ORDERBOOK_ERROR': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败' },
'ORDERBOOK_EMPTY': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空' },
'PRICE_RANGE': { color: 'purple', label: t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符' },
'UNKNOWN': { color: 'default', label: t('filteredOrdersList.filterTypes.unknown') || '未知原因' }
}
const config = typeMap[type] || typeMap['UNKNOWN']
@@ -210,26 +211,186 @@ const FilteredOrdersList: React.FC = () => {
<Option value="MARKET_STATUS">{t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易'}</Option>
<Option value="ORDERBOOK_ERROR">{t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败'}</Option>
<Option value="ORDERBOOK_EMPTY">{t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空'}</Option>
<Option value="PRICE_RANGE">{t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符'}</Option>
</Select>
</Space>
</div>
<Table
columns={columns}
dataSource={filteredOrders}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: limit,
total: total,
showSizeChanger: false,
showTotal: (total) => t('common.total') + `: ${total}`,
onChange: (page) => setPage(page)
}}
scroll={{ x: isMobile ? 800 : 'auto' }}
size={isMobile ? 'small' : 'middle'}
/>
{isMobile ? (
// 移动端卡片布局
<div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Spin size="large" />
</div>
) : filteredOrders.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
{t('filteredOrdersList.noData') || '暂无已过滤订单'}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{filteredOrders.map((order) => {
const date = new Date(order.createdAt)
const formattedDate = date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
const marketLink = getMarketLink(order)
const marketTitle = order.marketTitle || order.marketId.slice(0, 10) + '...'
return (
<Card
key={order.id}
style={{
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
border: '1px solid #e8e8e8'
}}
bodyStyle={{ padding: '16px' }}
>
{/* 市场信息 */}
<div style={{ marginBottom: '12px' }}>
<div style={{
fontSize: '16px',
fontWeight: 'bold',
marginBottom: '8px',
color: '#1890ff'
}}>
{marketLink ? (
<a href={marketLink} target="_blank" rel="noopener noreferrer" style={{ color: '#1890ff' }}>
{marketTitle}
</a>
) : (
marketTitle
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag color={order.side === 'BUY' ? 'green' : 'red'}>
{order.side === 'BUY' ? (t('order.buy') || '买入') : (t('order.sell') || '卖出')}
</Tag>
{getFilterTypeTag(order.filterType)}
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
{/* 订单详情 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{t('filteredOrdersList.outcome') || '市场方向'}
</div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>
{order.outcome || (order.outcomeIndex !== undefined ? `Index ${order.outcomeIndex}` : '-')}
</div>
</div>
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{t('filteredOrdersList.price') || '价格'}
</div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>
{order.price}
</div>
</div>
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{t('filteredOrdersList.size') || 'Leader数量'}
</div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>
{formatUSDC(order.size)}
</div>
</div>
{order.calculatedQuantity && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{t('filteredOrdersList.calculatedQuantity') || '计算数量'}
</div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>
{formatUSDC(order.calculatedQuantity)}
</div>
</div>
)}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{t('filteredOrdersList.filterReason') || '过滤原因'}
</div>
<div style={{ fontSize: '13px', color: '#333', wordBreak: 'break-word' }}>
{order.filterReason}
</div>
</div>
{/* 时间 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
{t('filteredOrdersList.createdAt') || '时间'}: {formattedDate}
</div>
</div>
</Card>
)
})}
</div>
)}
{/* 移动端分页 */}
{filteredOrders.length > 0 && (
<div style={{
marginTop: '16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '8px'
}}>
<div style={{ fontSize: '14px', color: '#666' }}>
{t('common.total') || '共'} {total} {t('common.items') || '条'}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
size="small"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
{t('common.prev') || '上一页'}
</Button>
<span style={{ lineHeight: '32px', fontSize: '14px' }}>
{page} / {Math.ceil(total / limit)}
</span>
<Button
size="small"
disabled={page >= Math.ceil(total / limit)}
onClick={() => setPage(page + 1)}
>
{t('common.next') || '下一页'}
</Button>
</div>
</div>
)}
</div>
) : (
// 桌面端表格布局
<Table
columns={columns}
dataSource={filteredOrders}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: limit,
total: total,
showSizeChanger: false,
showTotal: (total) => t('common.total') + `: ${total}`,
onChange: (page) => setPage(page)
}}
scroll={{ x: 'auto' }}
size="middle"
/>
)}
</Card>
</div>
)
+36 -2
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space } from 'antd'
import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space, Divider } from 'antd'
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useTranslation } from 'react-i18next'
@@ -54,7 +54,9 @@ const TemplateAdd: React.FC = () => {
supportSell: values.supportSell !== false,
minOrderDepth: values.minOrderDepth?.toString(),
maxSpread: values.maxSpread?.toString(),
minOrderbookDepth: values.minOrderbookDepth?.toString()
minOrderbookDepth: values.minOrderbookDepth?.toString(),
minPrice: values.minPrice?.toString(),
maxPrice: values.maxPrice?.toString()
})
if (response.data.code === 0) {
@@ -295,6 +297,38 @@ const TemplateAdd: React.FC = () => {
/>
</Form.Item>
<Divider>{t('templateAdd.priceRangeFilter') || '价格区间过滤'}</Divider>
<Form.Item
label={t('templateAdd.priceRange') || '价格区间'}
name="priceRange"
tooltip={t('templateAdd.priceRangeTooltip') || '配置价格区间,仅在指定价格区间内的订单才会下单。例如:0.11-0.89 表示区间在0.11和0.89之间;-0.89 表示0.89以下都可以;0.11- 表示0.11以上都可以'}
>
<Input.Group compact style={{ display: 'flex' }}>
<Form.Item name="minPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('templateAdd.minPricePlaceholder') || '最低价(可选)'}
/>
</Form.Item>
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
<Form.Item name="maxPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('templateAdd.maxPricePlaceholder') || '最高价(可选)'}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{/* 跟单卖出 - 表单最底部 */}
<Form.Item
label={t('templateAdd.supportSell') || '跟单卖出'}
+39 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space } from 'antd'
import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space, Divider } from 'antd'
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import type { CopyTradingTemplate } from '../types'
@@ -40,7 +40,9 @@ const TemplateEdit: React.FC = () => {
priceTolerance: parseFloat(template.priceTolerance),
minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined,
maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined,
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined,
minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined,
maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined
})
} else {
message.error(response.data.msg || t('templateEdit.fetchFailed') || '获取模板详情失败')
@@ -97,7 +99,9 @@ const TemplateEdit: React.FC = () => {
supportSell: values.supportSell,
minOrderDepth: values.minOrderDepth?.toString(),
maxSpread: values.maxSpread?.toString(),
minOrderbookDepth: values.minOrderbookDepth?.toString()
minOrderbookDepth: values.minOrderbookDepth?.toString(),
minPrice: values.minPrice?.toString(),
maxPrice: values.maxPrice?.toString()
})
if (response.data.code === 0) {
@@ -330,6 +334,38 @@ const TemplateEdit: React.FC = () => {
/>
</Form.Item>
<Divider>{t('templateEdit.priceRangeFilter') || '价格区间过滤'}</Divider>
<Form.Item
label={t('templateEdit.priceRange') || '价格区间'}
name="priceRange"
tooltip={t('templateEdit.priceRangeTooltip') || '配置价格区间,仅在指定价格区间内的订单才会下单。例如:0.11-0.89 表示区间在0.11和0.89之间;-0.89 表示0.89以下都可以;0.11- 表示0.11以上都可以'}
>
<Input.Group compact style={{ display: 'flex' }}>
<Form.Item name="minPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('templateEdit.minPricePlaceholder') || '最低价(可选)'}
/>
</Form.Item>
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
<Form.Item name="maxPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder={t('templateEdit.maxPricePlaceholder') || '最高价(可选)'}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{/* 跟单卖出 - 表单最底部 */}
<Form.Item
label={t('templateEdit.supportSell') || '跟单卖出'}
+88 -2
View File
@@ -71,7 +71,12 @@ const TemplateList: React.FC = () => {
minOrderSize: template.minOrderSize ? parseFloat(template.minOrderSize) : undefined,
maxDailyOrders: template.maxDailyOrders,
priceTolerance: parseFloat(template.priceTolerance),
supportSell: template.supportSell
supportSell: template.supportSell,
minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined,
maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined,
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined,
minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined,
maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined
})
setCopyModalVisible(true)
@@ -114,7 +119,12 @@ const TemplateList: React.FC = () => {
minOrderSize: values.copyMode === 'RATIO' ? values.minOrderSize?.toString() : undefined,
maxDailyOrders: values.maxDailyOrders,
priceTolerance: values.priceTolerance?.toString(),
supportSell: values.supportSell !== false
supportSell: values.supportSell !== false,
minOrderDepth: values.minOrderDepth?.toString(),
maxSpread: values.maxSpread?.toString(),
minOrderbookDepth: values.minOrderbookDepth?.toString(),
minPrice: values.minPrice?.toString(),
maxPrice: values.maxPrice?.toString()
})
if (response.data.code === 0) {
@@ -598,6 +608,82 @@ const TemplateList: React.FC = () => {
<Switch />
</Form.Item>
<Divider></Divider>
<Form.Item
label="最小订单深度 (USDC)"
name="minOrderDepth"
tooltip="最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性"
>
<InputNumber
min={0}
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder="例如:100(可选,不填写表示不启用)"
/>
</Form.Item>
<Form.Item
label="最大价差(绝对价格)"
name="maxSpread"
tooltip="最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单"
>
<InputNumber
min={0}
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder="例如:0.05(5美分,可选,不填写表示不启用)"
/>
</Form.Item>
<Form.Item
label="最小订单簿深度 (USDC)"
name="minOrderbookDepth"
tooltip="最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度"
>
<InputNumber
min={0}
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder="例如:50(可选,不填写表示不启用)"
/>
</Form.Item>
<Divider></Divider>
<Form.Item
label="价格区间"
name="priceRange"
tooltip="仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。"
>
<Input.Group compact style={{ display: 'flex' }}>
<Form.Item name="minPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder="最低价(留空不限制)"
/>
</Form.Item>
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
<Form.Item name="maxPrice" noStyle>
<InputNumber
min={0.01}
max={0.99}
step={0.0001}
precision={4}
style={{ width: '50%' }}
placeholder="最高价(留空不限制)"
/>
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item shouldUpdate>
{({ getFieldsError }) => {
const errors = getFieldsError()