diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt index 44ac456..07e28bd 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt @@ -23,6 +23,50 @@ class AccountController( private val logger = LoggerFactory.getLogger(AccountController::class.java) + /** + * 检查代理地址选项(用于导入前选择代理类型) + */ + @PostMapping("/check-proxy-options") + fun checkProxyOptions(@RequestBody request: CheckProxyOptionsRequest): ResponseEntity> { + return try { + if (request.walletAddress.isBlank()) { + return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_WALLET_ADDRESS_EMPTY, messageSource = messageSource)) + } + if (request.privateKey.isNullOrBlank() && request.mnemonic.isNullOrBlank()) { + return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "必须提供私钥或助记词", messageSource)) + } + + val result = runBlocking { accountService.checkProxyOptions(request) } + result.fold( + onSuccess = { response -> + ResponseEntity.ok(ApiResponse.success(response)) + }, + onFailure = { e -> + logger.error("检查代理地址选项失败: ${e.message}", e) + when (e) { + is IllegalArgumentException -> ResponseEntity.ok( + ApiResponse.error( + ErrorCode.PARAM_ERROR, + e.message, + messageSource + ) + ) + else -> ResponseEntity.ok( + ApiResponse.error( + ErrorCode.SERVER_ERROR, + e.message, + messageSource + ) + ) + } + } + ) + } catch (e: Exception) { + logger.error("检查代理地址选项异常: ${e.message}", e) + ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource)) + } + } + /** * 通过私钥导入账户 */ diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt index 5ecb923..6860298 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -11,6 +11,37 @@ data class AccountImportRequest( val walletType: String = "magic" // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包) ) +/** + * 检查代理地址选项请求 + */ +data class CheckProxyOptionsRequest( + val walletAddress: String, // EOA 地址(必需) + val privateKey: String? = null, // 私钥(加密,私钥导入时提供) + val mnemonic: String? = null // 助记词(加密,助记词导入时提供) +) + +/** + * 代理地址选项信息 + */ +data class ProxyOptionDto( + val walletType: String, // "magic" 或 "safe" + val proxyAddress: String, // 代理地址 + val descriptionKey: String, // 说明文案的多语言 key(如 "accountImport.proxyOption.magic.description") + val availableBalance: String, // 可用余额 + val positionBalance: String, // 仓位余额 + val totalBalance: String, // 总余额 + val positionCount: Int, // 持仓数量 + val hasAssets: Boolean, // 是否有资产(余额>0 或持仓>0) + val error: String? = null // 获取失败时的错误信息(可选) +) + +/** + * 检查代理地址选项响应 + */ +data class CheckProxyOptionsResponse( + val options: List // 代理地址选项列表(私钥导入返回2个,助记词返回1个) +) + /** * 账户更新请求 */ diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index 65abab8..aacd9ce 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.eq +import com.wrbug.polymarketbot.util.gt import com.wrbug.polymarketbot.util.JsonUtils import com.wrbug.polymarketbot.util.getEventSlug import com.wrbug.polymarketbot.service.common.PolymarketClobService @@ -171,6 +172,192 @@ class AccountService( } } + /** + * 检查代理地址选项(用于账户导入前选择代理类型) + * 私钥导入:返回 Magic 和 Safe 两个选项 + * 助记词导入:仅返回 Safe 选项 + */ + suspend fun checkProxyOptions(request: CheckProxyOptionsRequest): Result { + return try { + // 1. 验证钱包地址格式 + if (!isValidWalletAddress(request.walletAddress)) { + return Result.failure(IllegalArgumentException("无效的钱包地址格式")) + } + + // 2. 验证至少提供了私钥或助记词之一 + if (request.privateKey.isNullOrBlank() && request.mnemonic.isNullOrBlank()) { + return Result.failure(IllegalArgumentException("必须提供私钥或助记词")) + } + + val options = mutableListOf() + + // 3. 判断导入类型 + val isPrivateKeyImport = !request.privateKey.isNullOrBlank() + + if (isPrivateKeyImport) { + // 私钥导入:并行获取 Magic 和 Safe 代理地址及资产 + coroutineScope { + val magicDeferred = async { + try { + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "magic").getOrNull() + if (proxyAddress != null) { + val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() + ProxyOptionDto( + walletType = "magic", + proxyAddress = proxyAddress, + descriptionKey = "accountImport.proxyOption.magic.description", + availableBalance = balance?.availableBalance ?: "0", + positionBalance = balance?.positionBalance ?: "0", + totalBalance = balance?.totalBalance ?: "0", + positionCount = balance?.positions?.size ?: 0, + hasAssets = (balance?.availableBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positionBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positions?.isNotEmpty() == true), + error = null + ) + } else { + ProxyOptionDto( + walletType = "magic", + proxyAddress = "", + descriptionKey = "accountImport.proxyOption.magic.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取 Magic 代理地址失败" + ) + } + } catch (e: Exception) { + logger.warn("获取 Magic 代理地址或资产失败: ${e.message}", e) + ProxyOptionDto( + walletType = "magic", + proxyAddress = blockchainService.calculateMagicProxyAddress(request.walletAddress), + descriptionKey = "accountImport.proxyOption.magic.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取资产信息失败: ${e.message}" + ) + } + } + + val safeDeferred = async { + try { + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "safe").getOrNull() + if (proxyAddress != null) { + val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() + ProxyOptionDto( + walletType = "safe", + proxyAddress = proxyAddress, + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = balance?.availableBalance ?: "0", + positionBalance = balance?.positionBalance ?: "0", + totalBalance = balance?.totalBalance ?: "0", + positionCount = balance?.positions?.size ?: 0, + hasAssets = (balance?.availableBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positionBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positions?.isNotEmpty() == true), + error = null + ) + } else { + ProxyOptionDto( + walletType = "safe", + proxyAddress = "", + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取 Safe 代理地址失败" + ) + } + } catch (e: Exception) { + logger.warn("获取 Safe 代理地址或资产失败: ${e.message}", e) + ProxyOptionDto( + walletType = "safe", + proxyAddress = "", + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取资产信息失败: ${e.message}" + ) + } + } + + val magicOption = magicDeferred.await() + val safeOption = safeDeferred.await() + // Safe 在前,Magic 在后 + options.add(safeOption) + options.add(magicOption) + } + } else { + // 助记词导入:仅获取 Safe 代理地址及资产 + try { + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "safe").getOrNull() + if (proxyAddress != null) { + val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() + options.add( + ProxyOptionDto( + walletType = "safe", + proxyAddress = proxyAddress, + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = balance?.availableBalance ?: "0", + positionBalance = balance?.positionBalance ?: "0", + totalBalance = balance?.totalBalance ?: "0", + positionCount = balance?.positions?.size ?: 0, + hasAssets = (balance?.availableBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positionBalance?.toSafeBigDecimal()?.gt(BigDecimal.ZERO) == true) || + (balance?.positions?.isNotEmpty() == true), + error = null + ) + ) + } else { + options.add( + ProxyOptionDto( + walletType = "safe", + proxyAddress = "", + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取 Safe 代理地址失败" + ) + ) + } + } catch (e: Exception) { + logger.warn("获取 Safe 代理地址或资产失败: ${e.message}", e) + options.add( + ProxyOptionDto( + walletType = "safe", + proxyAddress = "", + descriptionKey = "accountImport.proxyOption.safe.description", + availableBalance = "0", + positionBalance = "0", + totalBalance = "0", + positionCount = 0, + hasAssets = false, + error = "获取资产信息失败: ${e.message}" + ) + ) + } + } + + Result.success(CheckProxyOptionsResponse(options = options)) + } catch (e: Exception) { + logger.error("检查代理地址选项失败: ${e.message}", e) + Result.failure(e) + } + } + /** * 更新账户信息 */ diff --git a/frontend/src/components/AccountImportForm.tsx b/frontend/src/components/AccountImportForm.tsx index b1b0a60..17c9937 100644 --- a/frontend/src/components/AccountImportForm.tsx +++ b/frontend/src/components/AccountImportForm.tsx @@ -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 = ({ const isMobile = useMediaQuery({ maxWidth: 768 }) const { importAccount, loading } = useAccountStore() const [importType, setImportType] = useState('privateKey') - const [walletType, setWalletType] = useState('safe') const [derivedAddress, setDerivedAddress] = useState('') const [addressError, setAddressError] = useState('') + const [proxyOptions, setProxyOptions] = useState([]) + const [selectedProxyType, setSelectedProxyType] = useState('') + const [loadingProxyOptions, setLoadingProxyOptions] = useState(false) + const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址 // 当私钥输入时,自动推导地址 const handlePrivateKeyChange = (e: React.ChangeEvent) => { @@ -45,6 +50,9 @@ const AccountImportForm: React.FC = ({ if (!privateKey) { setDerivedAddress('') setAddressError('') + setProxyOptions([]) + setSelectedProxyType('') + setStep('input') return } @@ -52,6 +60,9 @@ const AccountImportForm: React.FC = ({ if (!isValidPrivateKey(privateKey)) { setAddressError(t('accountImport.privateKeyInvalid')) setDerivedAddress('') + setProxyOptions([]) + setSelectedProxyType('') + setStep('input') return } @@ -62,9 +73,17 @@ const AccountImportForm: React.FC = ({ // 自动填充钱包地址字段 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 = ({ if (!mnemonic) { setDerivedAddress('') setAddressError('') + setProxyOptions([]) + setSelectedProxyType('') + setStep('input') return } @@ -81,6 +103,9 @@ const AccountImportForm: React.FC = ({ if (!isValidMnemonic(mnemonic)) { setAddressError(t('accountImport.mnemonicInvalid')) setDerivedAddress('') + setProxyOptions([]) + setSelectedProxyType('') + setStep('input') return } @@ -91,14 +116,85 @@ const AccountImportForm: React.FC = ({ // 自动填充钱包地址字段 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 = ({ // 如果用户手动输入了地址,验证是否与推导的地址一致 if (values.walletAddress) { if (values.walletAddress !== derivedAddressFromMnemonic) { - // 地址不匹配,使用推导的地址(因为私钥是从助记词导出的,必须使用对应的地址) walletAddress = derivedAddressFromMnemonic } else { - // 地址匹配,使用用户输入的地址 walletAddress = values.walletAddress } } else { - // 如果用户没有输入地址,使用推导的地址 walletAddress = derivedAddressFromMnemonic } } @@ -145,14 +238,13 @@ const AccountImportForm: React.FC = ({ 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 = ({ 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 = ({ value={importType} onChange={(e) => { setImportType(e.target.value) - setDerivedAddress('') - setAddressError('') - form.setFieldsValue({ walletAddress: '' }) }} > {t('accountImport.privateKey')} {t('accountImport.mnemonic')} - - - {t('accountImport.walletType')}{' '} - - - - - } - > - setWalletType(e.target.value)} - > - - {t('accountImport.walletTypeSafe')} - - - {t('accountImport.walletTypeMagic')} - - - {importType === 'privateKey' ? ( <> @@ -250,13 +311,14 @@ const AccountImportForm: React.FC = ({ } } ]} - help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')} + help={addressError || ''} validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''} > @@ -282,6 +344,7 @@ const AccountImportForm: React.FC = ({ @@ -302,13 +365,14 @@ const AccountImportForm: React.FC = ({ } } ]} - help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')} + help={addressError || ''} validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''} > @@ -334,11 +398,113 @@ const AccountImportForm: React.FC = ({ )} + {/* 请求代理地址时的 loading 提示 */} + {loadingProxyOptions && step === 'input' && ( + + + + {t('accountImport.loadingProxyOptions')} + + } + type="info" + showIcon={false} + style={{ marginBottom: '16px' }} + /> + + )} + + {/* 代理地址选项选择 */} + {step === 'select' && ( + { + if (!selectedProxyType) { + return Promise.reject(new Error(t('accountImport.proxyOptionRequired'))) + } + return Promise.resolve() + } + } + ]} + > + {loadingProxyOptions ? ( + + ) : ( + + {proxyOptions.map((option) => ( + setSelectedProxyType(option.walletType)} + style={{ + cursor: 'pointer', + border: selectedProxyType === option.walletType ? '2px solid #1890ff' : '1px solid #d9d9d9', + backgroundColor: selectedProxyType === option.walletType ? '#e6f7ff' : '#fff' + }} + > +
+
+
+ + + {t(`accountImport.proxyOption.${option.walletType}.title`)} + + {option.hasAssets && ( + + {t('accountImport.proxyOption.hasAssets')} + + )} + {option.error && ( + + {t('accountImport.proxyOption.error')} + + )} +
+
+ {t(`accountImport.proxyOption.${option.walletType}.description`)} +
+
+ {t('accountImport.proxyOption.proxyAddress')}: {option.proxyAddress || '-'} +
+ {option.error ? ( +
+ {option.error} +
+ ) : ( +
+ + {t('accountImport.proxyOption.availableBalance')}: {formatUSDC(option.availableBalance)} USDC + + + {t('accountImport.proxyOption.positionBalance')}: {formatUSDC(option.positionBalance)} USDC + + + {t('accountImport.proxyOption.totalBalance')}: {formatUSDC(option.totalBalance)} USDC + + + {t('accountImport.proxyOption.positionCount')}: {option.positionCount} + +
+ )} +
+
+
+ ))} +
+ )} +
+ )} + = ({ 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 = ({ } export default AccountImportForm - diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 86d7136..a571e13 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -215,7 +215,29 @@ "walletTypeHelp": "Web3 Wallet: Polymarket accounts connected via browser wallets like MetaMask\nMagic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.)", "walletTypeMagic": "Magic (Email/Social Login)", "walletTypeSafe": "Web3 Wallet", - "magicNotSupported": "" + "magicNotSupported": "", + "loadingProxyOptions": "Loading proxy addresses and asset information...", + "selectProxyOption": "Please select a proxy address", + "proxyOptionRequired": "Please select a proxy address", + "proxyOption": { + "magic": { + "description": "Email/Social Login Account (Magic)", + "title": "Magic Proxy Address" + }, + "safe": { + "description": "MetaMask Browser Wallet Account (Safe)", + "title": "Safe Proxy Address" + }, + "proxyAddress": "Proxy Address", + "availableBalance": "Available Balance", + "positionBalance": "Position Balance", + "totalBalance": "Total Balance", + "positionCount": "Position Count", + "noAssets": "No Assets", + "hasAssets": "Has Assets", + "error": "Failed to fetch", + "select": "Select this proxy address" + } }, "leader": { "title": "Leader Management", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 1e2871c..89996c2 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -215,7 +215,29 @@ "walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户", "walletTypeMagic": "Magic(邮箱/社交账号登录)", "walletTypeSafe": "Web3钱包", - "magicNotSupported": "" + "magicNotSupported": "", + "loadingProxyOptions": "正在获取代理地址和资产信息...", + "selectProxyOption": "请选择代理地址", + "proxyOptionRequired": "请选择一个代理地址", + "proxyOption": { + "magic": { + "description": "邮箱/社交账号登录账户(Magic)", + "title": "Magic 代理地址" + }, + "safe": { + "description": "MetaMask 等浏览器钱包账户(Safe)", + "title": "Safe 代理地址" + }, + "proxyAddress": "代理地址", + "availableBalance": "可用余额", + "positionBalance": "仓位余额", + "totalBalance": "总余额", + "positionCount": "持仓数量", + "noAssets": "无资产", + "hasAssets": "有资产", + "error": "获取失败", + "select": "选择此代理地址" + } }, "leader": { "title": "Leader 管理", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index 638fa60..1acb745 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -215,7 +215,29 @@ "walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶", "walletTypeMagic": "Magic(郵箱/社群帳號登入)", "walletTypeSafe": "Web3錢包", - "magicNotSupported": "" + "magicNotSupported": "", + "loadingProxyOptions": "正在獲取代理地址和資產信息...", + "selectProxyOption": "請選擇代理地址", + "proxyOptionRequired": "請選擇一個代理地址", + "proxyOption": { + "magic": { + "description": "郵箱/社群帳號登入帳戶(Magic)", + "title": "Magic 代理地址" + }, + "safe": { + "description": "MetaMask 等瀏覽器錢包帳戶(Safe)", + "title": "Safe 代理地址" + }, + "proxyAddress": "代理地址", + "availableBalance": "可用餘額", + "positionBalance": "倉位餘額", + "totalBalance": "總餘額", + "positionCount": "持倉數量", + "noAssets": "無資產", + "hasAssets": "有資產", + "error": "獲取失敗", + "select": "選擇此代理地址" + } }, "leader": { "title": "Leader 管理", diff --git a/frontend/src/pages/PositionList.tsx b/frontend/src/pages/PositionList.tsx index 93a83e8..ad722ea 100644 --- a/frontend/src/pages/PositionList.tsx +++ b/frontend/src/pages/PositionList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, useRef } from 'react' import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty, Modal, Form, Descriptions } from 'antd' import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons' import { useNavigate } from 'react-router-dom' @@ -62,10 +62,10 @@ const PositionList: React.FC = () => { } }, []) - // 当仓位数据变化时,更新可赎回统计 + // 当仓位数据变化时,静默更新可赎回统计(不显示loading状态) useEffect(() => { if (currentPositions.length > 0) { - fetchRedeemableSummary() + fetchRedeemableSummarySilently() } }, [currentPositions, selectedAccountId]) @@ -74,7 +74,19 @@ const PositionList: React.FC = () => { setCurrentPage(1) }, [positionFilter, selectedAccountId, searchKeyword]) - // 获取可赎回仓位统计 + // 静默获取可赎回仓位统计(不显示loading状态) + const fetchRedeemableSummarySilently = async () => { + try { + const response = await apiService.accounts.getRedeemableSummary({ accountId: selectedAccountId }) + if (response.data.code === 0 && response.data.data) { + setRedeemableSummary(response.data.data) + } + } catch (error: any) { + console.error('获取可赎回统计失败:', error) + } + } + + // 获取可赎回仓位统计(带loading状态,用于用户主动操作) const fetchRedeemableSummary = async () => { setLoadingRedeemableSummary(true) try { @@ -91,8 +103,9 @@ const PositionList: React.FC = () => { // 处理赎回按钮点击 const handleRedeemClick = async () => { - await fetchRedeemableSummary() setRedeemModalVisible(true) + // 打开模态框时重新获取最新数据 + fetchRedeemableSummary() } // 提交赎回 diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1a5a1a1..6325605 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -206,6 +206,12 @@ export const apiService = { * 账户管理 API */ accounts: { + /** + * 检查代理地址选项(导入前选择代理类型) + */ + checkProxyOptions: (data: any) => + apiClient.post>('/accounts/check-proxy-options', data), + /** * 导入账户 */ diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4a91c7b..ce1e0f9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -46,6 +46,37 @@ export interface AccountImportRequest { walletType?: string // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包) } +/** + * 检查代理地址选项请求 + */ +export interface CheckProxyOptionsRequest { + walletAddress: string // EOA 地址 + privateKey?: string // 私钥(加密,私钥导入时提供) + mnemonic?: string // 助记词(加密,助记词导入时提供) +} + +/** + * 代理地址选项信息 + */ +export interface ProxyOption { + walletType: string // "magic" 或 "safe" + proxyAddress: string // 代理地址 + descriptionKey: string // 说明文案的多语言 key + availableBalance: string // 可用余额 + positionBalance: string // 仓位余额 + totalBalance: string // 总余额 + positionCount: number // 持仓数量 + hasAssets: boolean // 是否有资产 + error?: string // 获取失败时的错误信息 +} + +/** + * 检查代理地址选项响应 + */ +export interface CheckProxyOptionsResponse { + options: ProxyOption[] // 代理地址选项列表 +} + /** * 账户更新请求 */