feat: 赎回批量执行与仓位页体验优化
- 赎回:同一账户多市场合并为一笔交易,减少 Relayer 调用次数 - 仓位页:可赎回统计静默刷新,赎回按钮不再常驻 loading - 账户导入表单、API、多语言等相关改动 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Form, Input, Button, Radio, Space, Alert, Tooltip } from 'antd'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Form, Input, Button, Radio, Space, Alert, Card, Spin, message } from 'antd'
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import {
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
getPrivateKeyFromMnemonic,
|
||||
isValidWalletAddress,
|
||||
isValidPrivateKey,
|
||||
isValidMnemonic
|
||||
isValidMnemonic,
|
||||
formatUSDC
|
||||
} from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { apiService } from '../services/api'
|
||||
import type { ProxyOption } from '../types'
|
||||
|
||||
type ImportType = 'privateKey' | 'mnemonic'
|
||||
type WalletType = 'magic' | 'safe'
|
||||
|
||||
interface AccountImportFormProps {
|
||||
form: any
|
||||
@@ -35,9 +37,12 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { importAccount, loading } = useAccountStore()
|
||||
const [importType, setImportType] = useState<ImportType>('privateKey')
|
||||
const [walletType, setWalletType] = useState<WalletType>('safe')
|
||||
const [derivedAddress, setDerivedAddress] = useState<string>('')
|
||||
const [addressError, setAddressError] = useState<string>('')
|
||||
const [proxyOptions, setProxyOptions] = useState<ProxyOption[]>([])
|
||||
const [selectedProxyType, setSelectedProxyType] = useState<string>('')
|
||||
const [loadingProxyOptions, setLoadingProxyOptions] = useState<boolean>(false)
|
||||
const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址
|
||||
|
||||
// 当私钥输入时,自动推导地址
|
||||
const handlePrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
@@ -45,6 +50,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
if (!privateKey) {
|
||||
setDerivedAddress('')
|
||||
setAddressError('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,6 +60,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
if (!isValidPrivateKey(privateKey)) {
|
||||
setAddressError(t('accountImport.privateKeyInvalid'))
|
||||
setDerivedAddress('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,9 +73,17 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
|
||||
// 自动填充钱包地址字段
|
||||
form.setFieldsValue({ walletAddress: address })
|
||||
|
||||
// 延迟获取代理选项(避免频繁请求)
|
||||
setTimeout(() => {
|
||||
fetchProxyOptions(address, privateKey, null)
|
||||
}, 500)
|
||||
} catch (error: any) {
|
||||
setAddressError(error.message || t('accountImport.addressError'))
|
||||
setDerivedAddress('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +93,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
if (!mnemonic) {
|
||||
setDerivedAddress('')
|
||||
setAddressError('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,6 +103,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
if (!isValidMnemonic(mnemonic)) {
|
||||
setAddressError(t('accountImport.mnemonicInvalid'))
|
||||
setDerivedAddress('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,14 +116,85 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
|
||||
// 自动填充钱包地址字段
|
||||
form.setFieldsValue({ walletAddress: address })
|
||||
|
||||
// 延迟获取代理选项(避免频繁请求)
|
||||
setTimeout(() => {
|
||||
fetchProxyOptions(address, null, mnemonic)
|
||||
}, 500)
|
||||
} catch (error: any) {
|
||||
setAddressError(error.message || t('accountImport.addressErrorMnemonic'))
|
||||
setDerivedAddress('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代理地址选项
|
||||
const fetchProxyOptions = async (walletAddress: string, privateKey: string | null, mnemonic: string | null) => {
|
||||
if (!walletAddress || (!privateKey && !mnemonic)) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingProxyOptions(true)
|
||||
try {
|
||||
const response = await apiService.accounts.checkProxyOptions({
|
||||
walletAddress,
|
||||
privateKey: privateKey || undefined,
|
||||
mnemonic: mnemonic || undefined
|
||||
})
|
||||
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const options = response.data.data.options || []
|
||||
setProxyOptions(options)
|
||||
|
||||
// 如果有选项,进入选择步骤
|
||||
if (options.length > 0) {
|
||||
setStep('select')
|
||||
// 如果有资产,默认选择第一个有资产的选项
|
||||
const hasAssetsOption = options.find(opt => opt.hasAssets)
|
||||
if (hasAssetsOption) {
|
||||
setSelectedProxyType(hasAssetsOption.walletType)
|
||||
} else {
|
||||
// 否则选择第一个选项
|
||||
setSelectedProxyType(options[0].walletType)
|
||||
}
|
||||
} else {
|
||||
setStep('input')
|
||||
message.warning(t('accountImport.proxyOption.error') || '未获取到代理地址选项')
|
||||
}
|
||||
} else {
|
||||
setProxyOptions([])
|
||||
setStep('input')
|
||||
message.error(response.data.msg || '获取代理地址选项失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
setProxyOptions([])
|
||||
setStep('input')
|
||||
message.error(error.message || '获取代理地址选项失败')
|
||||
} finally {
|
||||
setLoadingProxyOptions(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换导入方式时重置状态
|
||||
useEffect(() => {
|
||||
setDerivedAddress('')
|
||||
setAddressError('')
|
||||
setProxyOptions([])
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
form.setFieldsValue({ walletAddress: '', privateKey: '', mnemonic: '' })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importType])
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
// 如果还在输入步骤,需要先选择代理地址
|
||||
if (step === 'input' || !selectedProxyType) {
|
||||
return Promise.reject(new Error(t('accountImport.proxyOptionRequired')))
|
||||
}
|
||||
|
||||
let privateKey: string
|
||||
let walletAddress: string
|
||||
|
||||
@@ -124,14 +220,11 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
// 如果用户手动输入了地址,验证是否与推导的地址一致
|
||||
if (values.walletAddress) {
|
||||
if (values.walletAddress !== derivedAddressFromMnemonic) {
|
||||
// 地址不匹配,使用推导的地址(因为私钥是从助记词导出的,必须使用对应的地址)
|
||||
walletAddress = derivedAddressFromMnemonic
|
||||
} else {
|
||||
// 地址匹配,使用用户输入的地址
|
||||
walletAddress = values.walletAddress
|
||||
}
|
||||
} else {
|
||||
// 如果用户没有输入地址,使用推导的地址
|
||||
walletAddress = derivedAddressFromMnemonic
|
||||
}
|
||||
}
|
||||
@@ -145,14 +238,13 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
privateKey: privateKey,
|
||||
walletAddress: walletAddress,
|
||||
accountName: values.accountName,
|
||||
walletType: walletType
|
||||
walletType: selectedProxyType
|
||||
})
|
||||
|
||||
// 等待store更新
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 获取新添加的账户ID(通过API获取,因为store可能还没更新)
|
||||
const { apiService } = await import('../services/api')
|
||||
const accountsResponse = await apiService.accounts.list()
|
||||
if (accountsResponse.data.code === 0 && accountsResponse.data.data) {
|
||||
const newAccounts = accountsResponse.data.data.list || []
|
||||
@@ -160,11 +252,9 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
if (newAccount && onSuccess) {
|
||||
onSuccess(newAccount.id)
|
||||
} else if (onSuccess) {
|
||||
// 如果找不到账户,仍然调用onSuccess(可能在其他地方处理)
|
||||
onSuccess(0)
|
||||
}
|
||||
} else if (onSuccess) {
|
||||
// API调用失败,仍然调用onSuccess
|
||||
onSuccess(0)
|
||||
}
|
||||
|
||||
@@ -197,41 +287,12 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
value={importType}
|
||||
onChange={(e) => {
|
||||
setImportType(e.target.value)
|
||||
setDerivedAddress('')
|
||||
setAddressError('')
|
||||
form.setFieldsValue({ walletAddress: '' })
|
||||
}}
|
||||
>
|
||||
<Radio value="privateKey">{t('accountImport.privateKey')}</Radio>
|
||||
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
{t('accountImport.walletType')}{' '}
|
||||
<Tooltip
|
||||
title={t('accountImport.walletTypeHelp')}
|
||||
overlayInnerStyle={{ whiteSpace: 'pre-line', maxWidth: '300px' }}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Radio.Group
|
||||
value={walletType}
|
||||
onChange={(e) => setWalletType(e.target.value)}
|
||||
>
|
||||
<Radio value="safe">
|
||||
{t('accountImport.walletTypeSafe')}
|
||||
</Radio>
|
||||
<Radio value="magic">
|
||||
{t('accountImport.walletTypeMagic')}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{importType === 'privateKey' ? (
|
||||
<>
|
||||
@@ -250,13 +311,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
}
|
||||
}
|
||||
]}
|
||||
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
|
||||
help={addressError || ''}
|
||||
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={t('accountImport.privateKeyPlaceholder')}
|
||||
onChange={handlePrivateKeyChange}
|
||||
disabled={loadingProxyOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -282,6 +344,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
<Input
|
||||
placeholder={t('accountImport.walletAddressPlaceholder')}
|
||||
readOnly={!!derivedAddress}
|
||||
disabled={loadingProxyOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
@@ -302,13 +365,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
}
|
||||
}
|
||||
]}
|
||||
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
|
||||
help={addressError || ''}
|
||||
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder={t('accountImport.mnemonicPlaceholder')}
|
||||
onChange={handleMnemonicChange}
|
||||
disabled={loadingProxyOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -334,11 +398,113 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
<Input
|
||||
placeholder={t('accountImport.walletAddressPlaceholder')}
|
||||
readOnly={!!derivedAddress}
|
||||
disabled={loadingProxyOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 请求代理地址时的 loading 提示 */}
|
||||
{loadingProxyOptions && step === 'input' && (
|
||||
<Form.Item>
|
||||
<Alert
|
||||
message={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Spin size="small" />
|
||||
<span>{t('accountImport.loadingProxyOptions')}</span>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 代理地址选项选择 */}
|
||||
{step === 'select' && (
|
||||
<Form.Item
|
||||
label={t('accountImport.selectProxyOption')}
|
||||
required
|
||||
rules={[
|
||||
{
|
||||
validator: () => {
|
||||
if (!selectedProxyType) {
|
||||
return Promise.reject(new Error(t('accountImport.proxyOptionRequired')))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
{loadingProxyOptions ? (
|
||||
<Spin tip={t('accountImport.loadingProxyOptions')} />
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
{proxyOptions.map((option) => (
|
||||
<Card
|
||||
key={option.walletType}
|
||||
hoverable
|
||||
onClick={() => setSelectedProxyType(option.walletType)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedProxyType === option.walletType ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: selectedProxyType === option.walletType ? '#e6f7ff' : '#fff'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<Radio checked={selectedProxyType === option.walletType} />
|
||||
<strong style={{ marginLeft: '8px' }}>
|
||||
{t(`accountImport.proxyOption.${option.walletType}.title`)}
|
||||
</strong>
|
||||
{option.hasAssets && (
|
||||
<span style={{ marginLeft: '8px', color: '#52c41a' }}>
|
||||
<CheckCircleOutlined /> {t('accountImport.proxyOption.hasAssets')}
|
||||
</span>
|
||||
)}
|
||||
{option.error && (
|
||||
<span style={{ marginLeft: '8px', color: '#ff4d4f' }}>
|
||||
<ExclamationCircleOutlined /> {t('accountImport.proxyOption.error')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginLeft: '24px', color: '#666', fontSize: '14px', marginBottom: '8px' }}>
|
||||
{t(`accountImport.proxyOption.${option.walletType}.description`)}
|
||||
</div>
|
||||
<div style={{ marginLeft: '24px', fontSize: '12px', color: '#999', marginBottom: '4px' }}>
|
||||
{t('accountImport.proxyOption.proxyAddress')}: {option.proxyAddress || '-'}
|
||||
</div>
|
||||
{option.error ? (
|
||||
<div style={{ marginLeft: '24px', color: '#ff4d4f', fontSize: '12px' }}>
|
||||
{option.error}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginLeft: '24px', display: 'flex', gap: '16px', fontSize: '12px', color: '#666' }}>
|
||||
<span>
|
||||
{t('accountImport.proxyOption.availableBalance')}: {formatUSDC(option.availableBalance)} USDC
|
||||
</span>
|
||||
<span>
|
||||
{t('accountImport.proxyOption.positionBalance')}: {formatUSDC(option.positionBalance)} USDC
|
||||
</span>
|
||||
<span>
|
||||
{t('accountImport.proxyOption.totalBalance')}: {formatUSDC(option.totalBalance)} USDC
|
||||
</span>
|
||||
<span>
|
||||
{t('accountImport.proxyOption.positionCount')}: {option.positionCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('accountImport.accountName')}
|
||||
name="accountName"
|
||||
@@ -352,6 +518,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
disabled={step !== 'select' || !selectedProxyType || loadingProxyOptions}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
{t('accountImport.importAccount')}
|
||||
@@ -369,4 +536,3 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
}
|
||||
|
||||
export default AccountImportForm
|
||||
|
||||
|
||||
Reference in New Issue
Block a user