feat: 重构仓位轮训逻辑并修复多语言配置
主要改动: 1. 创建独立的 PositionCheckService 服务 - 将仓位检查逻辑从 PositionPushService 中分离 - 实现待赎回仓位检查和未卖出订单检查 - 实现订单状态更新逻辑(FIFO策略) 2. 修改 PositionPushService - 后端启动时自动启动轮训任务 - 每次轮训后推送全量数据给所有订阅的客户端 - 调用 PositionCheckService 进行仓位检查 3. 自动赎回功能调整 - 将自动赎回从跟单配置级别移到系统级别 - 添加系统级自动赎回配置管理 - 支持 Builder API Key 配置检查 4. 多语言配置修复 - 修复 PositionCheckService 中的硬编码消息 - 使用 MessageSource 获取多语言文本 - 添加自动赎回和 Builder API Key 相关的多语言资源 5. 数据库迁移 - V8: 添加跟单配置名称字段 - V9: 将自动赎回配置移到系统配置表 6. 前端更新 - 添加系统级自动赎回配置界面 - 更新跟单配置管理界面 - 添加 Builder API Key 配置界面
This commit is contained in:
+16
-2
@@ -92,7 +92,7 @@ function App() {
|
||||
* 处理订单推送消息,显示全局通知
|
||||
*/
|
||||
const handleOrderPush = useCallback((message: OrderPushMessage) => {
|
||||
const { accountName, order, orderDetail } = message
|
||||
const { accountName, order, orderDetail, leaderName, configName } = message
|
||||
|
||||
// 根据订单类型和操作类型确定通知内容
|
||||
const orderTypeText = getOrderTypeText(order.type)
|
||||
@@ -100,7 +100,21 @@ function App() {
|
||||
|
||||
// 如果有市场名称,在标题中显示
|
||||
const marketName = orderDetail?.marketName || order.market.substring(0, 8) + '...'
|
||||
const title = `${accountName} - ${orderTypeText}`
|
||||
|
||||
// 构建标题:如果是跟单订单,显示 leader 备注和跟单配置名
|
||||
let title = `${accountName} - ${orderTypeText}`
|
||||
if (leaderName || configName) {
|
||||
const parts: string[] = []
|
||||
if (configName) {
|
||||
parts.push(configName)
|
||||
}
|
||||
if (leaderName) {
|
||||
parts.push(`Leader: ${leaderName}`)
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
title = `${accountName} (${parts.join(', ')}) - ${orderTypeText}`
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用订单详情中的数据,如果没有则使用 WebSocket 消息中的数据
|
||||
const price = orderDetail ? parseFloat(orderDetail.price).toFixed(4) : parseFloat(order.price).toFixed(4)
|
||||
|
||||
@@ -304,6 +304,18 @@
|
||||
"systemSettingsDesc": "Configure proxy, view API health status",
|
||||
"footer": "Please use the above pages for configuration management."
|
||||
},
|
||||
"systemSettings": {
|
||||
"autoRedeem": {
|
||||
"title": "Auto Redeem Configuration",
|
||||
"label": "Auto Redeem",
|
||||
"tooltip": "When enabled, the system will automatically redeem redeemable positions. Requires Builder API Key configuration to take effect",
|
||||
"builderApiKeyNotConfigured": "Builder API Key Not Configured",
|
||||
"builderApiKeyNotConfiguredDesc": "Auto redeem feature requires Builder API Key configuration to take effect.",
|
||||
"goToConfigure": "Go to Configure",
|
||||
"saveSuccess": "Auto redeem configuration updated",
|
||||
"saveFailed": "Failed to update auto redeem configuration"
|
||||
}
|
||||
},
|
||||
"builderApiKey": {
|
||||
"title": "Builder API Key Configuration",
|
||||
"alertTitle": "What is Builder API Key?",
|
||||
@@ -326,6 +338,7 @@
|
||||
"saveSuccess": "Builder API Key configuration saved successfully",
|
||||
"saveFailed": "Failed to save Builder API Key configuration",
|
||||
"getFailed": "Failed to get Builder API Key configuration",
|
||||
"noChanges": "No fields to update",
|
||||
"notConfigured": "Builder API Key not configured, please go to System Settings to configure",
|
||||
"notConfiguredError": "Builder API Key not configured, cannot execute Gasless transactions. Please go to System Settings to configure Builder API Key.",
|
||||
"apiReference": "API Reference Documentation",
|
||||
@@ -654,6 +667,18 @@
|
||||
"priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.",
|
||||
"minPricePlaceholder": "Min Price (leave empty for no limit)",
|
||||
"maxPricePlaceholder": "Max Price (leave empty for no limit)",
|
||||
"configName": "Configuration Name",
|
||||
"configNameRequired": "Please enter configuration name",
|
||||
"configNamePlaceholder": "e.g., Copy Trading Config 1",
|
||||
"configNameTooltip": "Set a name for the copy trading configuration for easy identification and management",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"pushFailedOrders": "Push Failed Orders",
|
||||
"pushFailedOrdersTooltip": "When enabled, failed orders will be pushed to Telegram",
|
||||
"autoRedeem": "Auto Redeem",
|
||||
"autoRedeemTooltip": "When enabled, the system will automatically redeem redeemable positions. Requires Builder API Key configuration to take effect",
|
||||
"builderApiKeyNotConfigured": "Builder API Key Not Configured",
|
||||
"builderApiKeyNotConfiguredDesc": "Auto redeem feature requires Builder API Key configuration to take effect.",
|
||||
"goToConfigure": "Go to Configure",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders",
|
||||
"create": "Create Copy Trading Config",
|
||||
@@ -717,6 +742,18 @@
|
||||
"priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.",
|
||||
"minPricePlaceholder": "Min Price (leave empty for no limit)",
|
||||
"maxPricePlaceholder": "Max Price (leave empty for no limit)",
|
||||
"configName": "Configuration Name",
|
||||
"configNameRequired": "Please enter configuration name",
|
||||
"configNamePlaceholder": "e.g., Copy Trading Config 1",
|
||||
"configNameTooltip": "Set a name for the copy trading configuration for easy identification and management",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"pushFailedOrders": "Push Failed Orders",
|
||||
"pushFailedOrdersTooltip": "When enabled, failed orders will be pushed to Telegram",
|
||||
"autoRedeem": "Auto Redeem",
|
||||
"autoRedeemTooltip": "When enabled, the system will automatically redeem redeemable positions. Requires Builder API Key configuration to take effect",
|
||||
"builderApiKeyNotConfigured": "Builder API Key Not Configured",
|
||||
"builderApiKeyNotConfiguredDesc": "Auto redeem feature requires Builder API Key configuration to take effect.",
|
||||
"goToConfigure": "Go to Configure",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders",
|
||||
"save": "Save",
|
||||
|
||||
@@ -216,6 +216,18 @@
|
||||
"systemSettingsDesc": "配置代理、查看 API 健康状态",
|
||||
"footer": "请使用上述页面进行配置管理。"
|
||||
},
|
||||
"systemSettings": {
|
||||
"autoRedeem": {
|
||||
"title": "自动赎回配置",
|
||||
"label": "自动赎回",
|
||||
"tooltip": "开启后,系统会自动赎回可赎回的仓位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自动赎回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"saveSuccess": "自动赎回配置已更新",
|
||||
"saveFailed": "更新自动赎回配置失败"
|
||||
}
|
||||
},
|
||||
"builderApiKey": {
|
||||
"title": "Builder API Key 配置",
|
||||
"alertTitle": "什么是 Builder API Key?",
|
||||
@@ -238,6 +250,7 @@
|
||||
"saveSuccess": "保存 Builder API Key 配置成功",
|
||||
"saveFailed": "保存 Builder API Key 配置失败",
|
||||
"getFailed": "获取 Builder API Key 配置失败",
|
||||
"noChanges": "没有需要更新的字段",
|
||||
"notConfigured": "Builder API Key 未配置,请前往系统设置页面配置",
|
||||
"notConfiguredError": "Builder API Key 未配置,无法执行 Gasless 交易。请前往系统设置页面配置 Builder API Key。",
|
||||
"apiReference": "API 参考文档",
|
||||
@@ -508,6 +521,18 @@
|
||||
"copyTradingAdd": {
|
||||
"title": "新增跟单配置",
|
||||
"back": "返回",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "请输入配置名",
|
||||
"configNamePlaceholder": "例如:跟单配置1",
|
||||
"configNameTooltip": "为跟单配置设置一个名称,便于识别和管理",
|
||||
"advancedSettings": "高级设置",
|
||||
"pushFailedOrders": "推送失败订单",
|
||||
"pushFailedOrdersTooltip": "开启后,失败的订单会推送到 Telegram",
|
||||
"autoRedeem": "自动赎回",
|
||||
"autoRedeemTooltip": "开启后,系统会自动赎回可赎回的仓位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自动赎回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"selectWallet": "选择钱包",
|
||||
"selectWalletPlaceholder": "请选择钱包",
|
||||
"walletRequired": "请选择钱包",
|
||||
@@ -579,6 +604,18 @@
|
||||
"copyTradingEdit": {
|
||||
"title": "编辑跟单配置",
|
||||
"back": "返回",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "请输入配置名",
|
||||
"configNamePlaceholder": "例如:跟单配置1",
|
||||
"configNameTooltip": "为跟单配置设置一个名称,便于识别和管理",
|
||||
"advancedSettings": "高级设置",
|
||||
"pushFailedOrders": "推送失败订单",
|
||||
"pushFailedOrdersTooltip": "开启后,失败的订单会推送到 Telegram",
|
||||
"autoRedeem": "自动赎回",
|
||||
"autoRedeemTooltip": "开启后,系统会自动赎回可赎回的仓位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自动赎回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"wallet": "钱包",
|
||||
"leader": "Leader",
|
||||
"selectWallet": "钱包",
|
||||
@@ -666,6 +703,8 @@
|
||||
"copyTradingList": {
|
||||
"title": "跟单配置管理",
|
||||
"addCopyTrading": "新增跟单",
|
||||
"configName": "配置名",
|
||||
"configNameNotProvided": "未提供",
|
||||
"wallet": "钱包",
|
||||
"account": "账户",
|
||||
"template": "模板",
|
||||
|
||||
@@ -304,6 +304,18 @@
|
||||
"systemSettingsDesc": "配置代理、查看 API 健康狀態",
|
||||
"footer": "請使用上述頁面進行配置管理。"
|
||||
},
|
||||
"systemSettings": {
|
||||
"autoRedeem": {
|
||||
"title": "自動贖回配置",
|
||||
"label": "自動贖回",
|
||||
"tooltip": "開啟後,系統會自動贖回可贖回的倉位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自動贖回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"saveSuccess": "自動贖回配置已更新",
|
||||
"saveFailed": "更新自動贖回配置失敗"
|
||||
}
|
||||
},
|
||||
"builderApiKey": {
|
||||
"title": "Builder API Key 配置",
|
||||
"alertTitle": "什麼是 Builder API Key?",
|
||||
@@ -326,6 +338,7 @@
|
||||
"saveSuccess": "保存 Builder API Key 配置成功",
|
||||
"saveFailed": "保存 Builder API Key 配置失敗",
|
||||
"getFailed": "獲取 Builder API Key 配置失敗",
|
||||
"noChanges": "沒有需要更新的字段",
|
||||
"notConfigured": "Builder API Key 未配置,請前往系統設置頁面配置",
|
||||
"notConfiguredError": "Builder API Key 未配置,無法執行 Gasless 交易。請前往系統設置頁面配置 Builder API Key。",
|
||||
"apiReference": "API 參考文檔",
|
||||
@@ -654,6 +667,18 @@
|
||||
"priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。",
|
||||
"minPricePlaceholder": "最低價(留空不限制)",
|
||||
"maxPricePlaceholder": "最高價(留空不限制)",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "請輸入配置名",
|
||||
"configNamePlaceholder": "例如:跟單配置1",
|
||||
"configNameTooltip": "為跟單配置設置一個名稱,便於識別和管理",
|
||||
"advancedSettings": "高級設置",
|
||||
"pushFailedOrders": "推送失敗訂單",
|
||||
"pushFailedOrdersTooltip": "開啟後,失敗的訂單會推送到 Telegram",
|
||||
"autoRedeem": "自動贖回",
|
||||
"autoRedeemTooltip": "開啟後,系統會自動贖回可贖回的倉位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自動贖回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單",
|
||||
"create": "創建跟單配置",
|
||||
@@ -717,6 +742,18 @@
|
||||
"priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。",
|
||||
"minPricePlaceholder": "最低價(留空不限制)",
|
||||
"maxPricePlaceholder": "最高價(留空不限制)",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "請輸入配置名",
|
||||
"configNamePlaceholder": "例如:跟單配置1",
|
||||
"configNameTooltip": "為跟單配置設置一個名稱,便於識別和管理",
|
||||
"advancedSettings": "高級設置",
|
||||
"pushFailedOrders": "推送失敗訂單",
|
||||
"pushFailedOrdersTooltip": "開啟後,失敗的訂單會推送到 Telegram",
|
||||
"autoRedeem": "自動贖回",
|
||||
"autoRedeemTooltip": "開啟後,系統會自動贖回可贖回的倉位。需要配置 Builder API Key 才能生效",
|
||||
"builderApiKeyNotConfigured": "Builder API Key 未配置",
|
||||
"builderApiKeyNotConfiguredDesc": "自動贖回功能需要配置 Builder API Key 才能生效。",
|
||||
"goToConfigure": "前往配置",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單",
|
||||
"save": "保存",
|
||||
|
||||
@@ -25,11 +25,11 @@ const BuilderApiKeySettings: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const config = response.data.data
|
||||
setBuilderApiKeyConfig(config)
|
||||
// 预填充字段(如果已配置,显示占位符)
|
||||
// 如果已配置,输入框留空(不显示***)
|
||||
builderApiKeyForm.setFieldsValue({
|
||||
builderApiKey: config.builderApiKeyConfigured ? '***' : '',
|
||||
builderSecret: config.builderSecretConfigured ? '***' : '',
|
||||
builderPassphrase: config.builderPassphraseConfigured ? '***' : '',
|
||||
builderApiKey: '',
|
||||
builderSecret: '',
|
||||
builderPassphrase: '',
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || t('builderApiKey.getFailed'))
|
||||
@@ -42,16 +42,23 @@ const BuilderApiKeySettings: React.FC = () => {
|
||||
const handleBuilderApiKeySubmit = async (values: BuilderApiKeyUpdateRequest) => {
|
||||
setBuilderApiKeyLoading(true)
|
||||
try {
|
||||
// 如果值是 '***',表示已配置但未修改,不发送
|
||||
// 只发送非空字段(如果字段为空且已配置,表示不修改,不发送该字段)
|
||||
const updateData: BuilderApiKeyUpdateRequest = {}
|
||||
if (values.builderApiKey && values.builderApiKey !== '***') {
|
||||
updateData.builderApiKey = values.builderApiKey
|
||||
if (values.builderApiKey && values.builderApiKey.trim()) {
|
||||
updateData.builderApiKey = values.builderApiKey.trim()
|
||||
}
|
||||
if (values.builderSecret && values.builderSecret !== '***') {
|
||||
updateData.builderSecret = values.builderSecret
|
||||
if (values.builderSecret && values.builderSecret.trim()) {
|
||||
updateData.builderSecret = values.builderSecret.trim()
|
||||
}
|
||||
if (values.builderPassphrase && values.builderPassphrase !== '***') {
|
||||
updateData.builderPassphrase = values.builderPassphrase
|
||||
if (values.builderPassphrase && values.builderPassphrase.trim()) {
|
||||
updateData.builderPassphrase = values.builderPassphrase.trim()
|
||||
}
|
||||
|
||||
// 如果所有字段都为空,提示用户
|
||||
if (!updateData.builderApiKey && !updateData.builderSecret && !updateData.builderPassphrase) {
|
||||
message.warning(t('builderApiKey.noChanges') || '没有需要更新的字段')
|
||||
setBuilderApiKeyLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await apiService.systemConfig.updateBuilderApiKey(updateData)
|
||||
|
||||
@@ -22,10 +22,31 @@ const CopyTradingAdd: React.FC = () => {
|
||||
const [templateModalVisible, setTemplateModalVisible] = useState(false)
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
|
||||
// 生成默认配置名
|
||||
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}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchLeaders()
|
||||
fetchTemplates()
|
||||
|
||||
// 生成默认配置名
|
||||
const defaultConfigName = generateDefaultConfigName()
|
||||
form.setFieldsValue({ configName: defaultConfigName })
|
||||
}, [])
|
||||
|
||||
const fetchLeaders = async () => {
|
||||
@@ -114,7 +135,9 @@ const CopyTradingAdd: React.FC = () => {
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString(),
|
||||
minPrice: values.minPrice?.toString(),
|
||||
maxPrice: values.maxPrice?.toString()
|
||||
maxPrice: values.maxPrice?.toString(),
|
||||
configName: values.configName?.trim(),
|
||||
pushFailedOrders: values.pushFailedOrders ?? false
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.create(request)
|
||||
@@ -163,10 +186,26 @@ const CopyTradingAdd: React.FC = () => {
|
||||
useWebSocket: true,
|
||||
websocketReconnectInterval: 5000,
|
||||
websocketMaxRetries: 10,
|
||||
supportSell: true
|
||||
supportSell: true,
|
||||
pushFailedOrders: false
|
||||
}}
|
||||
>
|
||||
{/* 基础信息 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.configName') || '配置名'}
|
||||
name="configName"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingAdd.configNameRequired') || '请输入配置名' },
|
||||
{ whitespace: true, message: t('copyTradingAdd.configNameRequired') || '配置名不能为空' }
|
||||
]}
|
||||
tooltip={t('copyTradingAdd.configNameTooltip') || '为跟单配置设置一个名称,便于识别和管理'}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('copyTradingAdd.configNamePlaceholder') || '例如:跟单配置1'}
|
||||
maxLength={255}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectWallet') || '选择钱包'}
|
||||
name="accountId"
|
||||
@@ -444,7 +483,9 @@ const CopyTradingAdd: React.FC = () => {
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Divider>{t('copyTradingAdd.advancedSettings') || '高级设置'}</Divider>
|
||||
|
||||
{/* 跟单卖出 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
@@ -454,6 +495,16 @@ const CopyTradingAdd: React.FC = () => {
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 推送失败订单 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.pushFailedOrders') || '推送失败订单'}
|
||||
name="pushFailedOrders"
|
||||
tooltip={t('copyTradingAdd.pushFailedOrdersTooltip') || '开启后,失败的订单会推送到 Telegram'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -58,7 +58,9 @@ const CopyTradingEdit: React.FC = () => {
|
||||
maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined,
|
||||
minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined,
|
||||
minPrice: found.minPrice ? parseFloat(found.minPrice) : undefined,
|
||||
maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined
|
||||
maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined,
|
||||
configName: found.configName || '',
|
||||
pushFailedOrders: found.pushFailedOrders ?? false
|
||||
})
|
||||
} else {
|
||||
message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在')
|
||||
@@ -122,7 +124,9 @@ const CopyTradingEdit: React.FC = () => {
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString(),
|
||||
minPrice: values.minPrice?.toString(),
|
||||
maxPrice: values.maxPrice?.toString()
|
||||
maxPrice: values.maxPrice?.toString(),
|
||||
configName: values.configName?.trim() || undefined,
|
||||
pushFailedOrders: values.pushFailedOrders
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.update(request)
|
||||
@@ -172,6 +176,21 @@ const CopyTradingEdit: React.FC = () => {
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 基础信息(只读) */}
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.configName') || '配置名'}
|
||||
name="configName"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingEdit.configNameRequired') || '请输入配置名' },
|
||||
{ whitespace: true, message: t('copyTradingEdit.configNameRequired') || '配置名不能为空' }
|
||||
]}
|
||||
tooltip={t('copyTradingEdit.configNameTooltip') || '为跟单配置设置一个名称,便于识别和管理'}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('copyTradingEdit.configNamePlaceholder') || '例如:跟单配置1'}
|
||||
maxLength={255}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectWallet') || t('copyTradingEdit.selectWallet') || '钱包'}
|
||||
name="accountId"
|
||||
@@ -433,7 +452,9 @@ const CopyTradingEdit: React.FC = () => {
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Divider>{t('copyTradingEdit.advancedSettings') || '高级设置'}</Divider>
|
||||
|
||||
{/* 跟单卖出 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
@@ -443,6 +464,16 @@ const CopyTradingEdit: React.FC = () => {
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 推送失败订单 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.pushFailedOrders') || '推送失败订单'}
|
||||
name="pushFailedOrders"
|
||||
tooltip={t('copyTradingEdit.pushFailedOrdersTooltip') || '开启后,失败的订单会推送到 Telegram'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -146,6 +146,16 @@ const CopyTradingList: React.FC = () => {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('copyTradingList.configName') || '配置名',
|
||||
key: 'configName',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<div style={{ fontSize: isMobile ? 13 : 14, fontWeight: 500 }}>
|
||||
{record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingList.wallet') || '钱包',
|
||||
key: 'account',
|
||||
@@ -454,10 +464,17 @@ const CopyTradingList: React.FC = () => {
|
||||
{/* 基本信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
marginBottom: '8px',
|
||||
color: '#666'
|
||||
}}>
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingList.ratioMode') || '比例'} ${record.copyRatio}x`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert, Badge, Spin, Row, Col } from 'antd'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined, GlobalOutlined } from '@ant-design/icons'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined, GlobalOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
@@ -35,18 +36,23 @@ interface ApiHealthStatus {
|
||||
}
|
||||
|
||||
const SystemSettings: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [form] = Form.useForm()
|
||||
const [autoRedeemForm] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [checkResult, setCheckResult] = useState<ProxyCheckResponse | null>(null)
|
||||
const [currentConfig, setCurrentConfig] = useState<ProxyConfig | null>(null)
|
||||
const [apiHealthStatus, setApiHealthStatus] = useState<ApiHealthStatus[]>([])
|
||||
const [checkingApiHealth, setCheckingApiHealth] = useState(false)
|
||||
const [autoRedeemLoading, setAutoRedeemLoading] = useState(false)
|
||||
const [builderApiKeyConfigured, setBuilderApiKeyConfigured] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig()
|
||||
checkApiHealth()
|
||||
fetchSystemConfig()
|
||||
}, [])
|
||||
|
||||
const fetchConfig = async () => {
|
||||
@@ -161,6 +167,38 @@ const SystemSettings: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const response = await apiService.systemConfig.get()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const config = response.data.data
|
||||
setBuilderApiKeyConfigured(config.builderApiKeyConfigured)
|
||||
autoRedeemForm.setFieldsValue({
|
||||
autoRedeem: config.autoRedeem
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoRedeemSubmit = async (values: { autoRedeem: boolean }) => {
|
||||
setAutoRedeemLoading(true)
|
||||
try {
|
||||
const response = await apiService.systemConfig.updateAutoRedeem({ enabled: values.autoRedeem })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('systemSettings.autoRedeem.saveSuccess') || '自动赎回配置已更新')
|
||||
fetchSystemConfig()
|
||||
} else {
|
||||
message.error(response.data.msg || t('systemSettings.autoRedeem.saveFailed') || '更新自动赎回配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('systemSettings.autoRedeem.saveFailed') || '更新自动赎回配置失败')
|
||||
} finally {
|
||||
setAutoRedeemLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
@@ -389,6 +427,65 @@ const SystemSettings: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<SettingOutlined />
|
||||
<span>{t('systemSettings.autoRedeem.title') || '自动赎回配置'}</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: '16px' }}
|
||||
>
|
||||
<Form
|
||||
form={autoRedeemForm}
|
||||
layout="vertical"
|
||||
onFinish={handleAutoRedeemSubmit}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('systemSettings.autoRedeem.label') || '自动赎回'}
|
||||
name="autoRedeem"
|
||||
tooltip={t('systemSettings.autoRedeem.tooltip') || '开启后,系统会自动赎回可赎回的仓位。需要配置 Builder API Key 才能生效'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{!builderApiKeyConfigured && (
|
||||
<Alert
|
||||
message={t('systemSettings.autoRedeem.builderApiKeyNotConfigured') || 'Builder API Key 未配置'}
|
||||
description={
|
||||
<span>
|
||||
{t('systemSettings.autoRedeem.builderApiKeyNotConfiguredDesc') || '自动赎回功能需要配置 Builder API Key 才能生效。'}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => window.location.href = '/system-settings/builder-api-key'}
|
||||
style={{ padding: 0, marginLeft: '8px' }}
|
||||
>
|
||||
{t('systemSettings.autoRedeem.goToConfigure') || '前往配置'}
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={autoRedeemLoading}
|
||||
>
|
||||
{t('common.save') || '保存配置'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -594,7 +594,25 @@ export const apiService = {
|
||||
* 更新 Builder API Key 配置
|
||||
*/
|
||||
updateBuilderApiKey: (data: import('../types').BuilderApiKeyUpdateRequest) =>
|
||||
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/builder-api-key/update', data)
|
||||
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/builder-api-key/update', data),
|
||||
|
||||
/**
|
||||
* 检查 Builder API Key 是否已配置
|
||||
*/
|
||||
checkBuilderApiKey: () =>
|
||||
apiClient.post<ApiResponse<{ configured: boolean }>>('/system/config/builder-api-key/check', {}),
|
||||
|
||||
/**
|
||||
* 更新自动赎回配置
|
||||
*/
|
||||
updateAutoRedeem: (data: { enabled: boolean }) =>
|
||||
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/auto-redeem/update', data),
|
||||
|
||||
/**
|
||||
* 获取自动赎回状态
|
||||
*/
|
||||
getAutoRedeemStatus: () =>
|
||||
apiClient.post<ApiResponse<{ enabled: boolean }>>('/system/config/auto-redeem/status', {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,9 @@ export interface CopyTrading {
|
||||
minOrderbookDepth?: string
|
||||
minPrice?: string // 最低价格(可选),NULL表示不限制最低价
|
||||
maxPrice?: string // 最高价格(可选),NULL表示不限制最高价
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders: boolean // 推送失败订单(默认关闭)
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -246,6 +249,9 @@ export interface CopyTradingCreateRequest {
|
||||
minOrderbookDepth?: string
|
||||
minPrice?: string // 最低价格(可选),NULL表示不限制最低价
|
||||
maxPrice?: string // 最高价格(可选),NULL表示不限制最高价
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders?: boolean // 推送失败订单(可选)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,6 +281,9 @@ export interface CopyTradingUpdateRequest {
|
||||
minOrderbookDepth?: string
|
||||
minPrice?: string // 最低价格(可选),NULL表示不限制最低价
|
||||
maxPrice?: string // 最高价格(可选),NULL表示不限制最高价
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders?: boolean // 推送失败订单(可选)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,6 +506,9 @@ export interface OrderPushMessage {
|
||||
order: OrderMessage // 订单信息(来自 WebSocket)
|
||||
orderDetail?: OrderDetail // 订单详情(通过 API 获取)
|
||||
timestamp?: number // 推送时间戳
|
||||
// 跟单相关字段(可选,仅在跟单触发的订单时提供)
|
||||
leaderName?: string // Leader 名称(备注)
|
||||
configName?: string // 跟单配置名
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,6 +772,7 @@ export interface SystemConfig {
|
||||
builderApiKeyConfigured: boolean
|
||||
builderSecretConfigured: boolean
|
||||
builderPassphraseConfigured: boolean
|
||||
autoRedeem: boolean // 自动赎回(系统级别配置,默认开启)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user