feat: 账户设置后端 action、Safe 部署与设置引导弹窗复用
账户设置: - 后端: execute-setup-step API(步骤1 Safe 一键部署/步骤2 启用交易/步骤3 代币授权) - Safe 部署: Eip712Encoder SafeCreate/CreateProxy,RelayClientService.deploySafeViaBuilderRelayer - BuilderRelayerApi: SignatureParams 增加 paymentToken/payment/paymentReceiver,TransactionRequest.nonce 可选 - 导入成功有未完成步骤时先弹设置引导再关导入弹窗;设置弹窗复用 AccountSetupStatusBlock(embedded) - AccountSetupStatusBlock: 5s 轮询、embedded 模式、onAllCompleted、移除全部完成 Tag、授权信息始终展示且右对齐额度列 - 多语言: 步骤1/2/3 文案与 actionSuccess/actionFailed CryptoTail: - SpreadDirection/SpreadMode 枚举及 Converter,V38 迁移,策略列表与 api/types 调整 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { apiService } from '../services/api'
|
||||
import type { ProxyOption } from '../types'
|
||||
import AccountSetupGuideModal from './AccountSetupGuideModal'
|
||||
|
||||
type ImportType = 'privateKey' | 'mnemonic'
|
||||
|
||||
@@ -41,6 +42,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
const [selectedProxyType, setSelectedProxyType] = useState<string>('')
|
||||
const [loadingProxyOptions, setLoadingProxyOptions] = useState<boolean>(false)
|
||||
const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址
|
||||
const [setupModalVisible, setSetupModalVisible] = useState<boolean>(false)
|
||||
const [setupStatus, setSetupStatus] = useState<any>(null)
|
||||
const [importedAccountId, setImportedAccountId] = useState<number | undefined>(undefined)
|
||||
|
||||
// 当私钥输入时,自动推导地址(不支持换行,自动去除换行符)
|
||||
const handlePrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
@@ -254,11 +258,34 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
|
||||
// 获取新添加的账户ID(通过API获取,因为store可能还没更新)
|
||||
const accountsResponse = await apiService.accounts.list()
|
||||
let accountId: number | undefined = undefined
|
||||
if (accountsResponse.data.code === 0 && accountsResponse.data.data) {
|
||||
const newAccounts = accountsResponse.data.data.list || []
|
||||
const newAccount = newAccounts.find((acc: any) => acc.walletAddress === walletAddress)
|
||||
if (newAccount && onSuccess) {
|
||||
onSuccess(newAccount.id)
|
||||
if (newAccount) {
|
||||
accountId = newAccount.id
|
||||
setImportedAccountId(accountId)
|
||||
|
||||
// 检查账户设置状态
|
||||
let willShowSetupModal = false
|
||||
try {
|
||||
const setupResponse = await apiService.accounts.checkSetupStatus(accountId)
|
||||
if (setupResponse.data.code === 0 && setupResponse.data.data) {
|
||||
const status = setupResponse.data.data
|
||||
setSetupStatus(status)
|
||||
const hasIncomplete = !status.proxyDeployed || !status.tradingEnabled || !status.tokensApproved
|
||||
if (hasIncomplete) {
|
||||
setSetupModalVisible(true)
|
||||
willShowSetupModal = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查账户设置状态失败:', error)
|
||||
}
|
||||
// 未展示设置弹窗时才调用 onSuccess,避免父组件关闭导入弹窗导致设置弹窗被卸载
|
||||
if (!willShowSetupModal && onSuccess) {
|
||||
onSuccess(accountId)
|
||||
}
|
||||
} else if (onSuccess) {
|
||||
onSuccess(0)
|
||||
}
|
||||
@@ -551,6 +578,37 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 账户设置引导弹窗 */}
|
||||
<AccountSetupGuideModal
|
||||
visible={setupModalVisible}
|
||||
setupStatus={setupStatus}
|
||||
accountId={importedAccountId}
|
||||
onClose={() => {
|
||||
setSetupModalVisible(false)
|
||||
onSuccess?.(importedAccountId ?? 0)
|
||||
}}
|
||||
onComplete={async () => {
|
||||
// 刷新设置状态
|
||||
if (importedAccountId) {
|
||||
try {
|
||||
const setupResponse = await apiService.accounts.checkSetupStatus(importedAccountId)
|
||||
if (setupResponse.data.code === 0 && setupResponse.data.data) {
|
||||
setSetupStatus(setupResponse.data.data)
|
||||
const status = setupResponse.data.data
|
||||
// 如果所有步骤都完成了,关闭弹窗并通知父组件
|
||||
if (status.proxyDeployed && status.tradingEnabled && status.tokensApproved) {
|
||||
setSetupModalVisible(false)
|
||||
message.success(t('accountSetup.allCompleted.title'))
|
||||
onSuccess?.(importedAccountId ?? 0)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新设置状态失败:', error)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Modal, Alert, Space, Button, Typography } from 'antd'
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined, WalletOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import AccountSetupStatusBlock from './AccountSetupStatusBlock'
|
||||
import type { SetupStatus } from './AccountSetupStatusBlock'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
interface AccountSetupGuideModalProps {
|
||||
visible: boolean
|
||||
setupStatus: SetupStatus | null
|
||||
accountId?: number
|
||||
onClose: () => void
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
const AccountSetupGuideModal: React.FC<AccountSetupGuideModalProps> = ({
|
||||
visible,
|
||||
setupStatus: _initialStatus,
|
||||
accountId,
|
||||
onClose,
|
||||
onComplete
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [allCompleted, setAllCompleted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) setAllCompleted(false)
|
||||
}, [visible, accountId])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<WalletOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
|
||||
<span>{t('accountSetup.title')}</span>
|
||||
</Space>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
{allCompleted ? (
|
||||
<Button type="primary" onClick={onClose} size={isMobile ? 'middle' : 'large'}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onClose} size={isMobile ? 'middle' : 'large'}>
|
||||
{t('common.later')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={isMobile ? '95%' : 680}
|
||||
style={{ top: isMobile ? 20 : 50 }}
|
||||
destroyOnClose
|
||||
maskClosable={allCompleted}
|
||||
closable
|
||||
>
|
||||
<div style={{ padding: isMobile ? '16px 0' : '24px 0' }}>
|
||||
{allCompleted ? (
|
||||
<Alert
|
||||
message={t('accountSetup.allCompleted.title')}
|
||||
description={t('accountSetup.allCompleted.description')}
|
||||
type="success"
|
||||
icon={<CheckCircleOutlined />}
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message={t('accountSetup.incomplete.title')}
|
||||
description={t('accountSetup.incomplete.description')}
|
||||
type="warning"
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{accountId != null && accountId > 0 ? (
|
||||
<AccountSetupStatusBlock
|
||||
accountId={accountId}
|
||||
embedded
|
||||
showApprovalDetails
|
||||
onAllCompleted={() => setAllCompleted(true)}
|
||||
onRefresh={onComplete}
|
||||
/>
|
||||
) : (
|
||||
<Text type="secondary">{t('accountSetup.error.description')}</Text>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 24, padding: '12px', background: '#f5f5f5', borderRadius: '4px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('accountSetup.help')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSetupGuideModal
|
||||
@@ -0,0 +1,307 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Card, Steps, Button, Space, Tag, Spin, Typography, message } from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
WalletOutlined,
|
||||
KeyOutlined,
|
||||
SafetyOutlined,
|
||||
LinkOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { apiService } from '../services/api'
|
||||
|
||||
const { Paragraph, Text } = Typography
|
||||
|
||||
export interface SetupStatus {
|
||||
proxyDeployed: boolean
|
||||
tradingEnabled: boolean
|
||||
tokensApproved: boolean
|
||||
approvalDetails?: Record<string, string>
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface AccountSetupStatusBlockProps {
|
||||
accountId: number
|
||||
onRefresh?: () => void
|
||||
onAllCompleted?: () => void
|
||||
size?: 'small' | 'default'
|
||||
showApprovalDetails?: boolean
|
||||
/** 嵌入模式:不渲染 Card,仅渲染步骤与授权详情(供弹窗等复用) */
|
||||
embedded?: boolean
|
||||
}
|
||||
|
||||
/** 步骤 key 与步骤编号对应 */
|
||||
const STEP_KEYS = ['step1', 'step2', 'step3'] as const
|
||||
const stepKeyToNumber = (key: string): number =>
|
||||
STEP_KEYS.indexOf(key as typeof STEP_KEYS[number]) + 1
|
||||
|
||||
const AccountSetupStatusBlock: React.FC<AccountSetupStatusBlockProps> = ({
|
||||
accountId,
|
||||
onRefresh,
|
||||
onAllCompleted,
|
||||
size = 'default',
|
||||
showApprovalDetails = true,
|
||||
embedded = false
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [setupStatus, setSetupStatus] = useState<SetupStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
|
||||
const fetchStatus = async () => {
|
||||
if (accountId <= 0) return
|
||||
try {
|
||||
const response = await apiService.accounts.checkSetupStatus(accountId)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setSetupStatus(response.data.data)
|
||||
} else {
|
||||
setSetupStatus(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户设置状态失败:', error)
|
||||
setSetupStatus(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchStatus()
|
||||
}, [accountId])
|
||||
|
||||
// 每 5 秒轮询最新状态(首次加载完成后且存在未完成步骤时轮询,全部完成后停止)
|
||||
useEffect(() => {
|
||||
if (accountId <= 0 || setupStatus == null) return
|
||||
const allCompleted =
|
||||
setupStatus.proxyDeployed &&
|
||||
setupStatus.tradingEnabled &&
|
||||
setupStatus.tokensApproved
|
||||
if (allCompleted) return
|
||||
const timer = setInterval(() => {
|
||||
fetchStatus()
|
||||
}, 5000)
|
||||
return () => clearInterval(timer)
|
||||
}, [accountId, setupStatus?.proxyDeployed, setupStatus?.tradingEnabled, setupStatus?.tokensApproved])
|
||||
|
||||
// 全部完成时通知父组件(供弹窗等关闭或更新用)
|
||||
const allCompleted =
|
||||
setupStatus != null &&
|
||||
setupStatus.proxyDeployed &&
|
||||
setupStatus.tradingEnabled &&
|
||||
setupStatus.tokensApproved
|
||||
useEffect(() => {
|
||||
if (allCompleted) onAllCompleted?.()
|
||||
}, [allCompleted, onAllCompleted])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await fetchStatus()
|
||||
onRefresh?.()
|
||||
}
|
||||
|
||||
const handleStepAction = async (key: string) => {
|
||||
const stepNum = stepKeyToNumber(key)
|
||||
if (stepNum < 1) return
|
||||
setActionLoading(key)
|
||||
try {
|
||||
const response = await apiService.accounts.executeSetupStep(accountId, stepNum)
|
||||
const res = response.data
|
||||
if (res.code !== 0) {
|
||||
message.error(res.msg || t('accountSetup.actionFailed'))
|
||||
return
|
||||
}
|
||||
const data = res.data
|
||||
if (data?.redirectUrl) {
|
||||
window.open(data.redirectUrl, '_blank')
|
||||
}
|
||||
if (data?.success !== false) {
|
||||
await fetchStatus()
|
||||
onRefresh?.()
|
||||
if (data?.transactionHash) {
|
||||
message.success(t('accountSetup.actionSuccess'))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(t('accountSetup.actionFailed'))
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !setupStatus) {
|
||||
const loadingContent = (
|
||||
<div style={{ textAlign: 'center', padding: '24px 0' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)
|
||||
return embedded ? <div>{loadingContent}</div> : (
|
||||
<Card title={t('accountSetup.title')} size={size}>{loadingContent}</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!setupStatus) {
|
||||
const errorContent = (
|
||||
<>
|
||||
<Text type="secondary">{t('accountSetup.error.description')}</Text>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleRefresh}>
|
||||
{t('accountSetup.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return embedded ? <div>{errorContent}</div> : (
|
||||
<Card title={t('accountSetup.title')} size={size}>{errorContent}</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'step1',
|
||||
title: t('accountSetup.step1.title'),
|
||||
description: t('accountSetup.step1.description'),
|
||||
icon: <WalletOutlined />,
|
||||
completed: setupStatus.proxyDeployed,
|
||||
actionLabel: t('accountSetup.step1.action')
|
||||
},
|
||||
{
|
||||
key: 'step2',
|
||||
title: t('accountSetup.step2.title'),
|
||||
description: t('accountSetup.step2.description'),
|
||||
icon: <KeyOutlined />,
|
||||
completed: setupStatus.tradingEnabled,
|
||||
actionLabel: t('accountSetup.step2.action')
|
||||
},
|
||||
{
|
||||
key: 'step3',
|
||||
title: t('accountSetup.step3.title'),
|
||||
description: t('accountSetup.step3.description'),
|
||||
icon: <SafetyOutlined />,
|
||||
completed: setupStatus.tokensApproved,
|
||||
actionLabel: t('accountSetup.step3.action')
|
||||
}
|
||||
]
|
||||
|
||||
const stepsContent = (
|
||||
<>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={steps.findIndex(s => !s.completed)}
|
||||
size="small"
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
{steps.map((step) => (
|
||||
<Steps.Step
|
||||
key={step.key}
|
||||
title={
|
||||
<Space>
|
||||
<span>{step.title}</span>
|
||||
{step.completed ? (
|
||||
<Tag color="success" icon={<CheckCircleOutlined />}>
|
||||
{t('accountSetup.completed')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="warning" icon={<CloseCircleOutlined />}>
|
||||
{t('accountSetup.pending')}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Paragraph style={{ marginBottom: 8, fontSize: 14, color: '#666' }}>
|
||||
{step.description}
|
||||
</Paragraph>
|
||||
{!step.completed && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => handleStepAction(step.key)}
|
||||
loading={actionLoading === step.key}
|
||||
style={{ marginTop: 4 }}
|
||||
>
|
||||
{step.actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
icon={step.icon}
|
||||
status={step.completed ? 'finish' : 'process'}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
|
||||
{showApprovalDetails && setupStatus.approvalDetails && Object.keys(setupStatus.approvalDetails).length > 0 && (
|
||||
<div style={{ marginTop: 16, padding: '12px', background: '#fafafa', borderRadius: 4 }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>{t('accountSetup.approvalDetails.title')}</Text>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
{Object.entries(setupStatus.approvalDetails).map(([contract, allowance]) => {
|
||||
const isUnlimited = allowance === 'unlimited'
|
||||
const isApproved = isUnlimited || parseFloat(allowance) > 0
|
||||
const displayText = isUnlimited
|
||||
? t('accountSetup.approvalDetails.unlimited')
|
||||
: isApproved
|
||||
? `${parseFloat(allowance).toFixed(2)} USDC`
|
||||
: t('accountSetup.approvalDetails.notApproved')
|
||||
return (
|
||||
<div
|
||||
key={contract}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: 13,
|
||||
minHeight: 24
|
||||
}}
|
||||
>
|
||||
<span>{t(`accountSetup.approvalDetails.${contract}`) || contract}</span>
|
||||
<span style={{ minWidth: 100, textAlign: 'right' }}>{displayText}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{setupStatus.error && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="danger">{setupStatus.error}</Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
if (embedded) {
|
||||
return <div style={{ position: 'relative' }}>{stepsContent}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('accountSetup.title')}
|
||||
size={size}
|
||||
extra={
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
loading={refreshing}
|
||||
>
|
||||
{t('accountSetup.refresh')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{stepsContent}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSetupStatusBlock
|
||||
@@ -13,6 +13,7 @@
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"confirm": "Confirm",
|
||||
"later": "Later",
|
||||
"submit": "Submit",
|
||||
"reset": "Reset",
|
||||
"close": "Close",
|
||||
@@ -237,6 +238,51 @@
|
||||
"select": "Select this proxy address"
|
||||
}
|
||||
},
|
||||
"accountSetup": {
|
||||
"title": "Account Setup Check",
|
||||
"completed": "Completed",
|
||||
"pending": "Pending",
|
||||
"refresh": "Refresh Status",
|
||||
"allCompleted": {
|
||||
"title": "All Setup Completed",
|
||||
"description": "Your account is ready to use all features."
|
||||
},
|
||||
"incomplete": {
|
||||
"title": "Account Setup Incomplete",
|
||||
"description": "Please complete the following setup steps to ensure your account works properly."
|
||||
},
|
||||
"step1": {
|
||||
"title": "Deploy Proxy Wallet",
|
||||
"description": "Proxy wallet is required for trading on Polymarket. Safe accounts can deploy with one click; Magic accounts will be redirected to Polymarket.",
|
||||
"action": "Deploy Proxy Wallet"
|
||||
},
|
||||
"step2": {
|
||||
"title": "Enable Trading",
|
||||
"description": "API credentials are required for trading. Click the button below to let the system automatically obtain and save the API Key.",
|
||||
"action": "Enable Trading (One-Click)"
|
||||
},
|
||||
"step3": {
|
||||
"title": "Approve Tokens",
|
||||
"description": "You need to authorize the proxy wallet to use your USDC tokens. Click the button below to complete token approval automatically.",
|
||||
"action": "Approve Tokens (One-Click)"
|
||||
},
|
||||
"approvalDetails": {
|
||||
"title": "Token Approval Details",
|
||||
"CTF_CONTRACT": "CTF Contract",
|
||||
"CTF_EXCHANGE": "CTF Exchange",
|
||||
"NEG_RISK_EXCHANGE": "Neg Risk Exchange",
|
||||
"NEG_RISK_ADAPTER": "Neg Risk Adapter",
|
||||
"notApproved": "Not Approved",
|
||||
"unlimited": "Unlimited"
|
||||
},
|
||||
"error": {
|
||||
"title": "Check Failed",
|
||||
"description": "Unable to check account setup status, please try again later."
|
||||
},
|
||||
"actionSuccess": "Operation successful",
|
||||
"actionFailed": "Operation failed, please try again later",
|
||||
"help": "Tip: After completing the setup, click the \"Refresh Status\" button to update the check results. If all steps are completed, you can use account features normally."
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader Management",
|
||||
"leaderName": "Leader Name",
|
||||
@@ -1452,13 +1498,17 @@
|
||||
"update": "Update",
|
||||
"timeWindowStartLEEnd": "Window start must not be greater than end",
|
||||
"timeWindowExceed": "Time window must not exceed period length",
|
||||
"minSpreadMode": "Min spread",
|
||||
"minSpreadModeTip": "Whether to place an order is based on the spread between open and close in the current period. Auto: system computes a suggested spread from the last 20 klines (updated each period); Fixed: you enter a value (e.g. 30), order only when spread ≥ that value; None: no spread check, order when price is in range.",
|
||||
"minSpreadModeNone": "None",
|
||||
"minSpreadModeFixed": "Fixed",
|
||||
"minSpreadModeAuto": "Auto",
|
||||
"minSpreadValue": "Min spread value (USDC)",
|
||||
"minSpreadValuePlaceholder": "e.g. 30"
|
||||
"spreadMode": "Spread",
|
||||
"spreadModeTip": "Whether to place an order is based on the spread between open and close in the current period. Auto: system computes a suggested spread from the last 20 klines (updated each period); Fixed: you enter a value (e.g. 30); None: no spread check, order when price is in range.",
|
||||
"spreadModeNone": "None",
|
||||
"spreadModeFixed": "Fixed",
|
||||
"spreadModeAuto": "Auto",
|
||||
"spreadValue": "Spread value (USDC)",
|
||||
"spreadValuePlaceholder": "e.g. 30",
|
||||
"spreadDirection": "Spread Direction",
|
||||
"spreadDirectionTip": "Min spread: trigger when spread ≥ configured value, buy price fixed at 0.99; Max spread: trigger when spread ≤ configured value, buy price = trigger price + 0.02 (suitable for low-price buying).",
|
||||
"spreadDirectionMin": "Min Spread",
|
||||
"spreadDirectionMax": "Max Spread"
|
||||
},
|
||||
"redeemRequiredModal": {
|
||||
"title": "Configure Auto Redeem First",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"later": "稍后",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"viewDetail": "查看详情",
|
||||
@@ -236,6 +237,51 @@
|
||||
"select": "选择此代理地址"
|
||||
}
|
||||
},
|
||||
"accountSetup": {
|
||||
"title": "账户设置检查",
|
||||
"completed": "已完成",
|
||||
"pending": "待完成",
|
||||
"refresh": "刷新状态",
|
||||
"allCompleted": {
|
||||
"title": "所有设置已完成",
|
||||
"description": "您的账户已准备就绪,可以开始使用所有功能。"
|
||||
},
|
||||
"incomplete": {
|
||||
"title": "账户设置未完成",
|
||||
"description": "请完成以下设置步骤,以确保账户可以正常使用。"
|
||||
},
|
||||
"step1": {
|
||||
"title": "部署代理钱包",
|
||||
"description": "代理钱包是您在 Polymarket 上进行交易的必要组件。Safe 账户可点击下方按钮由系统一键部署;Magic 账户将跳转至 Polymarket 完成。",
|
||||
"action": "部署代理钱包"
|
||||
},
|
||||
"step2": {
|
||||
"title": "启用交易",
|
||||
"description": "需要配置 API 凭证才能进行交易。点击下方按钮由系统自动获取并保存 API Key。",
|
||||
"action": "一键启用交易"
|
||||
},
|
||||
"step3": {
|
||||
"title": "批准代币",
|
||||
"description": "需要授权代理钱包使用您的 USDC 代币。点击下方按钮由系统自动完成代币授权。",
|
||||
"action": "一键批准代币"
|
||||
},
|
||||
"approvalDetails": {
|
||||
"title": "代币授权详情",
|
||||
"CTF_CONTRACT": "CTF 合约",
|
||||
"CTF_EXCHANGE": "CTF 交易所",
|
||||
"NEG_RISK_EXCHANGE": "负风险交易所",
|
||||
"NEG_RISK_ADAPTER": "负风险适配器",
|
||||
"notApproved": "未授权",
|
||||
"unlimited": "无限"
|
||||
},
|
||||
"error": {
|
||||
"title": "检查失败",
|
||||
"description": "无法检查账户设置状态,请稍后重试。"
|
||||
},
|
||||
"actionSuccess": "操作成功",
|
||||
"actionFailed": "操作失败,请稍后重试",
|
||||
"help": "提示:完成设置后,点击「刷新状态」按钮更新检查结果。如果所有步骤都已完成,您可以正常使用账户功能。"
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader 管理",
|
||||
"leaderName": "Leader 名称",
|
||||
@@ -1451,13 +1497,17 @@
|
||||
"update": "更新",
|
||||
"timeWindowStartLEEnd": "时间区间开始不能大于结束",
|
||||
"timeWindowExceed": "时间区间不能超过周期长度",
|
||||
"minSpreadMode": "最小价差",
|
||||
"minSpreadModeTip": "根据当前周期开盘价与收盘价的价差决定是否下单。自动:系统按历史 20 根 K 线计算建议价差(每周期更新);固定:您输入一个数值(如 30),仅当价差 ≥ 该值时才下单;无:不校验价差,满足价格区间即下单。",
|
||||
"minSpreadModeNone": "无",
|
||||
"minSpreadModeFixed": "固定",
|
||||
"minSpreadModeAuto": "自动",
|
||||
"minSpreadValue": "最小价差数值 (USDC)",
|
||||
"minSpreadValuePlaceholder": "如 30"
|
||||
"spreadMode": "价差",
|
||||
"spreadModeTip": "根据当前周期开盘价与收盘价的价差决定是否下单。自动:系统按历史 20 根 K 线计算建议价差(每周期更新);固定:您输入一个数值(如 30);无:不校验价差,满足价格区间即下单。",
|
||||
"spreadModeNone": "无",
|
||||
"spreadModeFixed": "固定",
|
||||
"spreadModeAuto": "自动",
|
||||
"spreadValue": "价差数值 (USDC)",
|
||||
"spreadValuePlaceholder": "如 30",
|
||||
"spreadDirection": "价差方向",
|
||||
"spreadDirectionTip": "最小价差:价差 ≥ 配置值时触发,买入价固定 0.99;最大价差:价差 ≤ 配置值时触发,买入价 = 触发价 + 0.02(适合低价买入)。",
|
||||
"spreadDirectionMin": "最小价差",
|
||||
"spreadDirectionMax": "最大价差"
|
||||
},
|
||||
"redeemRequiredModal": {
|
||||
"title": "请先配置自动赎回",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"success": "成功",
|
||||
"failed": "失敗",
|
||||
"confirm": "確認",
|
||||
"later": "稍後",
|
||||
"submit": "提交",
|
||||
"reset": "重置",
|
||||
"close": "關閉",
|
||||
@@ -237,6 +238,51 @@
|
||||
"select": "選擇此代理地址"
|
||||
}
|
||||
},
|
||||
"accountSetup": {
|
||||
"title": "帳戶設置檢查",
|
||||
"completed": "已完成",
|
||||
"pending": "待完成",
|
||||
"refresh": "刷新狀態",
|
||||
"allCompleted": {
|
||||
"title": "所有設置已完成",
|
||||
"description": "您的帳戶已準備就緒,可以開始使用所有功能。"
|
||||
},
|
||||
"incomplete": {
|
||||
"title": "帳戶設置未完成",
|
||||
"description": "請完成以下設置步驟,以確保帳戶可以正常使用。"
|
||||
},
|
||||
"step1": {
|
||||
"title": "部署代理錢包",
|
||||
"description": "代理錢包是您在 Polymarket 上進行交易的必要組件。Safe 帳戶可點擊下方按鈕由系統一鍵部署;Magic 帳戶將跳轉至 Polymarket 完成。",
|
||||
"action": "部署代理錢包"
|
||||
},
|
||||
"step2": {
|
||||
"title": "啟用交易",
|
||||
"description": "需要配置 API 憑證才能進行交易。點擊下方按鈕由系統自動獲取並保存 API Key。",
|
||||
"action": "一鍵啟用交易"
|
||||
},
|
||||
"step3": {
|
||||
"title": "批准代幣",
|
||||
"description": "需要授權代理錢包使用您的 USDC 代幣。點擊下方按鈕由系統自動完成代幣授權。",
|
||||
"action": "一鍵批准代幣"
|
||||
},
|
||||
"approvalDetails": {
|
||||
"title": "代幣授權詳情",
|
||||
"CTF_CONTRACT": "CTF 合約",
|
||||
"CTF_EXCHANGE": "CTF 交易所",
|
||||
"NEG_RISK_EXCHANGE": "負風險交易所",
|
||||
"NEG_RISK_ADAPTER": "負風險適配器",
|
||||
"notApproved": "未授權",
|
||||
"unlimited": "無限"
|
||||
},
|
||||
"error": {
|
||||
"title": "檢查失敗",
|
||||
"description": "無法檢查帳戶設置狀態,請稍後重試。"
|
||||
},
|
||||
"actionSuccess": "操作成功",
|
||||
"actionFailed": "操作失敗,請稍後重試",
|
||||
"help": "提示:完成設置後,點擊「刷新狀態」按鈕更新檢查結果。如果所有步驟都已完成,您可以正常使用帳戶功能。"
|
||||
},
|
||||
"leader": {
|
||||
"title": "Leader 管理",
|
||||
"leaderName": "Leader 名稱",
|
||||
@@ -1452,13 +1498,17 @@
|
||||
"update": "更新",
|
||||
"timeWindowStartLEEnd": "時間區間開始不能大於結束",
|
||||
"timeWindowExceed": "時間區間不能超過週期長度",
|
||||
"minSpreadMode": "最小價差",
|
||||
"minSpreadModeTip": "依當前週期開盤價與收盤價的價差決定是否下單。自動:系統依歷史 20 根 K 線計算建議價差(每週期更新);固定:您輸入一個數值(如 30),僅當價差 ≥ 該值時才下單;無:不校驗價差,滿足價格區間即下單。",
|
||||
"minSpreadModeNone": "無",
|
||||
"minSpreadModeFixed": "固定",
|
||||
"minSpreadModeAuto": "自動",
|
||||
"minSpreadValue": "最小價差數值 (USDC)",
|
||||
"minSpreadValuePlaceholder": "如 30"
|
||||
"spreadMode": "價差",
|
||||
"spreadModeTip": "依當前週期開盤價與收盤價的價差決定是否下單。自動:系統依歷史 20 根 K 線計算建議價差(每週期更新);固定:您輸入一個數值(如 30);無:不校驗價差,滿足價格區間即下單。",
|
||||
"spreadModeNone": "無",
|
||||
"spreadModeFixed": "固定",
|
||||
"spreadModeAuto": "自動",
|
||||
"spreadValue": "價差數值 (USDC)",
|
||||
"spreadValuePlaceholder": "如 30",
|
||||
"spreadDirection": "價差方向",
|
||||
"spreadDirectionTip": "最小價差:價差 ≥ 配置值時觸發,買入價固定 0.99;最大價差:價差 ≤ 配置值時觸發,買入價 = 觸發價 + 0.02(適合低價買入)。",
|
||||
"spreadDirectionMin": "最小價差",
|
||||
"spreadDirectionMax": "最大價差"
|
||||
},
|
||||
"redeemRequiredModal": {
|
||||
"title": "請先配置自動贖回",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
import AccountSetupStatusBlock from '../components/AccountSetupStatusBlock'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
@@ -150,10 +151,7 @@ const AccountDetail: React.FC = () => {
|
||||
onClick={() => {
|
||||
setEditModalVisible(true)
|
||||
editForm.setFieldsValue({
|
||||
accountName: account.accountName || '',
|
||||
apiKey: '', // 不显示实际值,留空表示不修改
|
||||
apiSecret: '', // 不显示实际值,留空表示不修改
|
||||
apiPassphrase: '' // 不显示实际值,留空表示不修改
|
||||
accountName: account.accountName || ''
|
||||
})
|
||||
}}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
@@ -214,46 +212,23 @@ const AccountDetail: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card
|
||||
title={t('account.apiCredentials')}
|
||||
style={{
|
||||
|
||||
{accountId && (
|
||||
<div style={{
|
||||
marginTop: isMobile ? '12px' : '16px',
|
||||
margin: isMobile ? '0 -8px' : '0',
|
||||
borderRadius: isMobile ? '0' : undefined
|
||||
}}
|
||||
>
|
||||
<Descriptions
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ fontSize: isMobile ? '14px' : undefined }}
|
||||
>
|
||||
<Descriptions.Item label={t('account.apiKey')}>
|
||||
<Tag color={account.apiKeyConfigured ? 'success' : 'default'}>
|
||||
{account.apiKeyConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('account.apiSecret')}>
|
||||
<Tag color={account.apiSecretConfigured ? 'success' : 'default'}>
|
||||
{account.apiSecretConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('account.apiPassphrase')}>
|
||||
<Tag color={account.apiPassphraseConfigured ? 'success' : 'default'}>
|
||||
{account.apiPassphraseConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('account.apiCredentials')}>
|
||||
{account.apiKeyConfigured && account.apiSecretConfigured && account.apiPassphraseConfigured ? (
|
||||
<Tag color="success">{t('account.fullConfig')}</Tag>
|
||||
) : (
|
||||
<Tag color="warning">{t('account.partialConfig')}</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
margin: isMobile ? '0 -8px' : '0'
|
||||
}}>
|
||||
<AccountSetupStatusBlock
|
||||
accountId={Number(accountId)}
|
||||
onRefresh={() => { loadAccountDetail(); loadBalance() }}
|
||||
size={isMobile ? 'small' : 'default'}
|
||||
showApprovalDetails={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider style={{ margin: isMobile ? '12px 0' : '16px 0' }} />
|
||||
|
||||
{(account.totalOrders !== undefined || account.totalPnl !== undefined ||
|
||||
account.activeOrders !== undefined ||
|
||||
account.completedOrders !== undefined || account.positionCount !== undefined) ? (
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Account } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
import AccountImportForm from '../components/AccountImportForm'
|
||||
import AccountSetupStatusBlock from '../components/AccountSetupStatusBlock'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
@@ -204,10 +205,7 @@ const AccountList: React.FC = () => {
|
||||
setEditAccount(accountDetail)
|
||||
|
||||
editForm.setFieldsValue({
|
||||
accountName: accountDetail.accountName || '',
|
||||
apiKey: '', // 不显示实际值,留空表示不修改
|
||||
apiSecret: '', // 不显示实际值,留空表示不修改
|
||||
apiPassphrase: '' // 不显示实际值,留空表示不修改
|
||||
accountName: accountDetail.accountName || ''
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('打开编辑失败:', error)
|
||||
@@ -720,35 +718,14 @@ const AccountList: React.FC = () => {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Descriptions
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
title={t('accountList.apiCredentials')}
|
||||
>
|
||||
<Descriptions.Item label={t('accountList.apiKey')}>
|
||||
<Tag color={detailAccount.apiKeyConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiKeyConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('accountList.apiSecret')}>
|
||||
<Tag color={detailAccount.apiSecretConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiSecretConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('accountList.apiPassphrase')}>
|
||||
<Tag color={detailAccount.apiPassphraseConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiPassphraseConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('accountList.configStatus')}>
|
||||
{detailAccount.apiKeyConfigured && detailAccount.apiSecretConfigured && detailAccount.apiPassphraseConfigured ? (
|
||||
<Tag color="success">{t('accountList.fullConfig')}</Tag>
|
||||
) : (
|
||||
<Tag color="warning">{t('accountList.partialConfig')}</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<AccountSetupStatusBlock
|
||||
accountId={detailAccount.id}
|
||||
onRefresh={handleRefreshDetailBalance}
|
||||
size={isMobile ? 'small' : 'default'}
|
||||
showApprovalDetails={true}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{(detailAccount.totalOrders !== undefined || detailAccount.totalPnl !== undefined ||
|
||||
detailAccount.activeOrders !== undefined ||
|
||||
|
||||
@@ -149,7 +149,8 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
enabled: true,
|
||||
amountMode: 'RATIO',
|
||||
maxPrice: '1',
|
||||
minSpreadMode: 'AUTO',
|
||||
spreadMode: 'AUTO',
|
||||
spreadDirection: 'MIN',
|
||||
windowStartMinutes: 0,
|
||||
windowStartSeconds: 0
|
||||
})
|
||||
@@ -170,8 +171,9 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
maxPrice: record.maxPrice,
|
||||
amountMode: record.amountMode,
|
||||
amountValue: record.amountValue,
|
||||
minSpreadMode: record.minSpreadMode ?? 'AUTO',
|
||||
minSpreadValue: record.minSpreadValue ?? undefined,
|
||||
spreadMode: record.spreadMode ?? 'AUTO',
|
||||
spreadValue: record.spreadValue ?? undefined,
|
||||
spreadDirection: record.spreadDirection ?? 'MIN',
|
||||
enabled: record.enabled
|
||||
})
|
||||
setFormModalOpen(true)
|
||||
@@ -204,8 +206,9 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
maxPrice: v.maxPrice != null ? String(v.maxPrice) : undefined,
|
||||
amountMode: v.amountMode as string,
|
||||
amountValue: String(v.amountValue ?? 0),
|
||||
minSpreadMode: (v.minSpreadMode as string) || 'AUTO',
|
||||
minSpreadValue: v.minSpreadMode === 'FIXED' && v.minSpreadValue != null ? String(v.minSpreadValue) : (v.minSpreadMode === 'AUTO' && v.minSpreadValue != null ? String(v.minSpreadValue) : undefined),
|
||||
spreadMode: (v.spreadMode as string) || 'AUTO',
|
||||
spreadValue: v.spreadMode === 'FIXED' && v.spreadValue != null ? String(v.spreadValue) : (v.spreadMode === 'AUTO' && v.spreadValue != null ? String(v.spreadValue) : undefined),
|
||||
spreadDirection: v.spreadDirection as string || 'MIN',
|
||||
enabled: v.enabled !== false
|
||||
}
|
||||
if (editingId) {
|
||||
@@ -218,8 +221,9 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
maxPrice: payload.maxPrice,
|
||||
amountMode: payload.amountMode,
|
||||
amountValue: payload.amountValue,
|
||||
minSpreadMode: payload.minSpreadMode,
|
||||
minSpreadValue: payload.minSpreadValue,
|
||||
spreadMode: payload.spreadMode,
|
||||
spreadValue: payload.spreadValue,
|
||||
spreadDirection: payload.spreadDirection,
|
||||
enabled: payload.enabled
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
@@ -232,7 +236,7 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
} else {
|
||||
const res = await apiService.cryptoTailStrategy.create({
|
||||
...payload,
|
||||
minSpreadValue: payload.minSpreadMode === 'FIXED' ? payload.minSpreadValue : undefined
|
||||
spreadValue: payload.spreadMode === 'FIXED' ? payload.spreadValue : undefined
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
message.success(t('common.success'))
|
||||
@@ -740,7 +744,7 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
destroyOnClose
|
||||
>
|
||||
<Alert type="warning" showIcon message={t('cryptoTailStrategy.form.walletTip')} style={{ marginBottom: 16 }} />
|
||||
<Form form={form} layout="vertical" initialValues={{ amountMode: 'RATIO', maxPrice: '1', minSpreadMode: 'AUTO', enabled: true }}>
|
||||
<Form form={form} layout="vertical" initialValues={{ amountMode: 'RATIO', maxPrice: '1', spreadMode: 'AUTO', spreadDirection: 'MIN', enabled: true }}>
|
||||
<Form.Item name="accountId" label={t('cryptoTailStrategy.form.selectAccount')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder={t('cryptoTailStrategy.form.selectAccount')}
|
||||
@@ -834,37 +838,37 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="minSpreadMode"
|
||||
name="spreadMode"
|
||||
label={
|
||||
<Space size={4}>
|
||||
<span>{t('cryptoTailStrategy.form.minSpreadMode')}</span>
|
||||
<Tooltip title={t('cryptoTailStrategy.form.minSpreadModeTip')}>
|
||||
<span>{t('cryptoTailStrategy.form.spreadMode')}</span>
|
||||
<Tooltip title={t('cryptoTailStrategy.form.spreadModeTip')}>
|
||||
<InfoCircleOutlined style={{ color: '#999', cursor: 'help', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="AUTO">{t('cryptoTailStrategy.form.minSpreadModeAuto')}</Radio>
|
||||
<Radio value="FIXED">{t('cryptoTailStrategy.form.minSpreadModeFixed')}</Radio>
|
||||
<Radio value="NONE">{t('cryptoTailStrategy.form.minSpreadModeNone')}</Radio>
|
||||
<Radio value="AUTO">{t('cryptoTailStrategy.form.spreadModeAuto')}</Radio>
|
||||
<Radio value="FIXED">{t('cryptoTailStrategy.form.spreadModeFixed')}</Radio>
|
||||
<Radio value="NONE">{t('cryptoTailStrategy.form.spreadModeNone')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, curr) => prev.minSpreadMode !== curr.minSpreadMode}
|
||||
shouldUpdate={(prev, curr) => prev.spreadMode !== curr.spreadMode}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('minSpreadMode') === 'FIXED' ? (
|
||||
getFieldValue('spreadMode') === 'FIXED' ? (
|
||||
<Form.Item
|
||||
name="minSpreadValue"
|
||||
label={t('cryptoTailStrategy.form.minSpreadValue')}
|
||||
name="spreadValue"
|
||||
label={t('cryptoTailStrategy.form.spreadValue')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
placeholder={t('cryptoTailStrategy.form.minSpreadValuePlaceholder')}
|
||||
placeholder={t('cryptoTailStrategy.form.spreadValuePlaceholder')}
|
||||
style={{ width: '100%' }}
|
||||
stringMode
|
||||
/>
|
||||
@@ -872,6 +876,22 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="spreadDirection"
|
||||
label={
|
||||
<Space size={4}>
|
||||
<span>{t('cryptoTailStrategy.form.spreadDirection')}</span>
|
||||
<Tooltip title={t('cryptoTailStrategy.form.spreadDirectionTip')}>
|
||||
<InfoCircleOutlined style={{ color: '#999', cursor: 'help', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="MIN">{t('cryptoTailStrategy.form.spreadDirectionMin')}</Radio>
|
||||
<Radio value="MAX">{t('cryptoTailStrategy.form.spreadDirectionMax')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name="enabled" valuePropName="checked">
|
||||
<Switch checkedChildren={t('common.enabled')} unCheckedChildren={t('common.disabled')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -215,13 +215,28 @@ export const apiService = {
|
||||
/**
|
||||
* 导入账户
|
||||
*/
|
||||
import: (data: any) =>
|
||||
import: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/accounts/import', data),
|
||||
|
||||
/**
|
||||
* 检查账户设置状态
|
||||
*/
|
||||
checkSetupStatus: (accountId: number) =>
|
||||
apiClient.post<ApiResponse<any>>('/accounts/check-setup-status', { accountId }),
|
||||
|
||||
/**
|
||||
* 执行设置步骤(步骤1 返回跳转 URL,步骤2/3 由后端执行)
|
||||
*/
|
||||
executeSetupStep: (accountId: number, step: number) =>
|
||||
apiClient.post<ApiResponse<{ success: boolean; redirectUrl?: string; transactionHash?: string }>>(
|
||||
'/accounts/execute-setup-step',
|
||||
{ accountId, step }
|
||||
),
|
||||
|
||||
/**
|
||||
* 更新账户
|
||||
*/
|
||||
update: (data: any) =>
|
||||
update: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/accounts/update', data),
|
||||
|
||||
/**
|
||||
@@ -447,8 +462,9 @@ export const apiService = {
|
||||
maxPrice?: string
|
||||
amountMode: string
|
||||
amountValue: string
|
||||
minSpreadMode?: string
|
||||
minSpreadValue?: string | null
|
||||
spreadMode?: string
|
||||
spreadValue?: string | null
|
||||
spreadDirection?: string
|
||||
enabled?: boolean
|
||||
}) =>
|
||||
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/create', data),
|
||||
@@ -461,8 +477,9 @@ export const apiService = {
|
||||
maxPrice?: string
|
||||
amountMode?: string
|
||||
amountValue?: string
|
||||
minSpreadMode?: string
|
||||
minSpreadValue?: string | null
|
||||
spreadMode?: string
|
||||
spreadValue?: string | null
|
||||
spreadDirection?: string
|
||||
enabled?: boolean
|
||||
}) =>
|
||||
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/update', data),
|
||||
|
||||
@@ -1051,10 +1051,12 @@ export interface CryptoTailStrategyDto {
|
||||
maxPrice: string
|
||||
amountMode: string
|
||||
amountValue: string
|
||||
/** 最小价差模式: NONE, FIXED, AUTO */
|
||||
minSpreadMode?: string
|
||||
/** 最小价差数值(FIXED 时必填;AUTO 时可为计算值) */
|
||||
minSpreadValue?: string | null
|
||||
/** 价差模式: NONE, FIXED, AUTO */
|
||||
spreadMode?: string
|
||||
/** 价差数值 */
|
||||
spreadValue?: string | null
|
||||
/** 价差方向: MIN=最小价差(价差>=配置值触发), MAX=最大价差(价差<=配置值触发) */
|
||||
spreadDirection?: string
|
||||
enabled: boolean
|
||||
lastTriggerAt?: number
|
||||
/** 已实现总收益 USDC */
|
||||
|
||||
Reference in New Issue
Block a user