feat(frontend): 优化导入账户弹窗与文案

- 导入弹窗:增加步骤条、导入方式改为按钮、代理选项卡片紧凑展示
- 代理选项:标题改为「请选择账户类型」,地址完整显示,有资产标绿,小白说明放入卡片内
- 私钥/助记词:输入框默认两行、禁止换行(换行自动转空格/去除)
- 多语言:新增 proxyAddressHelp,更新 selectProxyOption 文案
- Modal 宽度与内边距微调;PositionList 移除未使用 useRef

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-14 01:23:29 +08:00
parent 3405a1cda3
commit 72de65d670
7 changed files with 111 additions and 79 deletions
+99 -70
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Form, Input, Button, Radio, Space, Card, Spin, message, Alert } from 'antd'
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { Form, Input, Button, Radio, Space, Card, Spin, message, Alert, Steps, Tag } from 'antd'
import { KeyOutlined, WalletOutlined, UserOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
import {
@@ -42,9 +42,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
const [loadingProxyOptions, setLoadingProxyOptions] = useState<boolean>(false)
const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址
// 当私钥输入时,自动推导地址
// 当私钥输入时,自动推导地址(不支持换行,自动去除换行符)
const handlePrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const privateKey = e.target.value.trim()
const raw = e.target.value
const normalized = raw.replace(/\r?\n/g, '')
if (normalized !== raw) {
form.setFieldsValue({ privateKey: normalized })
}
const privateKey = normalized.trim()
if (!privateKey) {
setDerivedAddress('')
setAddressError('')
@@ -85,9 +90,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
}
}
// 当助记词输入时,自动推导地址
// 当助记词输入时,自动推导地址(不支持换行,换行符转为空格)
const handleMnemonicChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const mnemonic = e.target.value.trim()
const raw = e.target.value
const normalized = raw.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trimStart()
if (/\r?\n/.test(raw)) {
form.setFieldsValue({ mnemonic: normalized })
}
const mnemonic = normalized.trim()
if (!mnemonic) {
setDerivedAddress('')
setAddressError('')
@@ -150,7 +160,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
if (options.length > 0) {
setStep('select')
// 如果有资产,默认选择第一个有资产的选项
const hasAssetsOption = options.find(opt => opt.hasAssets)
const hasAssetsOption = options.find((opt: ProxyOption) => opt.hasAssets)
if (hasAssetsOption) {
setSelectedProxyType(hasAssetsOption.walletType)
} else {
@@ -262,23 +272,38 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
}
}
const currentStep = step === 'input' ? 0 : 1
return (
<>
<div style={{ padding: isMobile ? '0 4px' : '0 8px' }}>
<Steps
current={currentStep}
size="small"
style={{ marginBottom: 24 }}
items={[
{ title: t('accountImport.importMethod'), icon: <KeyOutlined /> },
{ title: t('accountImport.selectProxyOption'), icon: <WalletOutlined /> },
{ title: t('accountImport.accountName'), icon: <UserOutlined /> }
]}
/>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
size={isMobile ? 'middle' : 'large'}
>
<Form.Item label={t('accountImport.importMethod')}>
<Form.Item label={t('accountImport.importMethod')} style={{ marginBottom: 16 }}>
<Radio.Group
value={importType}
onChange={(e) => {
setImportType(e.target.value)
}}
optionType="button"
buttonStyle="solid"
size={isMobile ? 'middle' : 'large'}
>
<Radio value="privateKey">{t('accountImport.privateKey')}</Radio>
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio>
<Radio.Button value="privateKey">{t('accountImport.privateKey')}</Radio.Button>
<Radio.Button value="mnemonic">{t('accountImport.mnemonic')}</Radio.Button>
</Radio.Group>
</Form.Item>
@@ -303,9 +328,10 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
>
<Input.TextArea
rows={3}
rows={2}
placeholder={t('accountImport.privateKeyPlaceholder')}
onChange={handlePrivateKeyChange}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
disabled={loadingProxyOptions}
/>
</Form.Item>
@@ -357,9 +383,10 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
>
<Input.TextArea
rows={4}
rows={2}
placeholder={t('accountImport.mnemonicPlaceholder')}
onChange={handleMnemonicChange}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
disabled={loadingProxyOptions}
/>
</Form.Item>
@@ -397,14 +424,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
<Form.Item>
<Alert
message={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Spin size="small" />
<span>{t('accountImport.loadingProxyOptions')}</span>
</div>
}
type="info"
showIcon={false}
style={{ marginBottom: '16px' }}
style={{ marginBottom: 16 }}
/>
</Form.Item>
)}
@@ -424,70 +451,70 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
}
}
]}
style={{ marginBottom: 20 }}
>
{loadingProxyOptions ? (
<Spin tip={t('accountImport.loadingProxyOptions')} />
<div style={{ padding: '32px 0', textAlign: 'center' }}>
<Spin tip={t('accountImport.loadingProxyOptions')} />
</div>
) : (
<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>
<Space direction="vertical" style={{ width: '100%' }} size={12}>
{proxyOptions.map((option) => {
const isSelected = selectedProxyType === option.walletType
const typeLabel = option.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'
return (
<Card
key={option.walletType}
hoverable
onClick={() => setSelectedProxyType(option.walletType)}
size="small"
style={{
cursor: 'pointer',
borderColor: isSelected ? 'var(--ant-color-primary)' : undefined,
borderWidth: isSelected ? 2 : 1,
backgroundColor: isSelected ? 'var(--ant-color-primary-bg)' : undefined,
transition: 'border-color 0.2s, background-color 0.2s'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
<Space size="middle">
<Radio checked={isSelected} />
<Tag color={option.walletType.toLowerCase() === 'magic' ? 'purple' : 'blue'}>
{typeLabel}
</Tag>
{option.hasAssets && (
<span style={{ marginLeft: '8px', color: '#52c41a' }}>
<span style={{ color: '#52c41a', fontSize: 12 }}>
<CheckCircleOutlined /> {t('accountImport.proxyOption.hasAssets')}
</span>
)}
{option.error && (
<span style={{ marginLeft: '8px', color: '#ff4d4f' }}>
<span style={{ color: 'var(--ant-color-error)', fontSize: 12 }}>
<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>
</Space>
{!option.error && (
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ant-color-primary)' }}>
{formatUSDC(option.totalBalance)} USDC
</span>
)}
</div>
</div>
</Card>
))}
<div style={{ marginTop: 8, marginLeft: 28, fontSize: 12, color: 'var(--ant-color-text-secondary)', wordBreak: 'break-all' }}>
{option.proxyAddress ? (
<span style={{ fontFamily: 'monospace' }}>{option.proxyAddress}</span>
) : (
'-'
)}
{option.error && (
<span style={{ color: 'var(--ant-color-error)', marginLeft: 8 }}>{option.error}</span>
)}
</div>
<div style={{ marginTop: 8, marginLeft: 28, fontSize: 12, color: 'var(--ant-color-text-secondary)', lineHeight: 1.5 }}>
{t('accountImport.proxyOption.proxyAddressHelp')}
</div>
</Card>
)
})}
</Space>
)}
</Form.Item>
@@ -496,30 +523,32 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
<Form.Item
label={t('accountImport.accountName')}
name="accountName"
style={{ marginBottom: 24 }}
>
<Input placeholder={t('accountImport.accountNamePlaceholder')} />
</Form.Item>
<Form.Item>
<Space>
<Form.Item style={{ marginBottom: 0 }}>
<Space size="middle">
<Button
type="primary"
htmlType="submit"
loading={loading}
disabled={step !== 'select' || !selectedProxyType || loadingProxyOptions}
size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: 44 } : undefined}
>
{t('accountImport.importAccount')}
</Button>
{showCancelButton && onCancel && (
<Button onClick={onCancel}>
<Button onClick={onCancel} size={isMobile ? 'middle' : 'large'}>
{t('common.cancel')}
</Button>
)}
</Space>
</Form.Item>
</Form>
</>
</div>
)
}
+2 -1
View File
@@ -214,7 +214,7 @@
"walletType": "Wallet Type",
"walletTypeHelp": "Web3 Wallet: Polymarket accounts connected via browser wallets like MetaMask\nMagic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.)",
"loadingProxyOptions": "Loading proxy addresses and asset information...",
"selectProxyOption": "Please select a proxy address",
"selectProxyOption": "Please select account type",
"proxyOptionRequired": "Please select a proxy address",
"proxyOption": {
"magic": {
@@ -226,6 +226,7 @@
"title": "Safe Proxy Address"
},
"proxyAddress": "Proxy Address",
"proxyAddressHelp": "The proxy address is the wallet address you actually use on Polymarket: Magic for email/social login accounts, Safe for browser wallet (e.g. MetaMask) accounts. Please select the one that matches how you log in to Polymarket.",
"availableBalance": "Available Balance",
"positionBalance": "Position Balance",
"totalBalance": "Total Balance",
+2 -1
View File
@@ -213,7 +213,7 @@
"walletType": "钱包类型",
"walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户",
"loadingProxyOptions": "正在获取代理地址和资产信息...",
"selectProxyOption": "请选择代理地址",
"selectProxyOption": "请选择账户类型",
"proxyOptionRequired": "请选择一个代理地址",
"proxyOption": {
"magic": {
@@ -225,6 +225,7 @@
"title": "Safe 代理地址"
},
"proxyAddress": "代理地址",
"proxyAddressHelp": "代理地址是您在 Polymarket 上实际使用的钱包地址:Magic 为邮箱/社交登录账户,Safe 为浏览器钱包(如 MetaMask)账户。请选择与您 Polymarket 登录方式一致的一项。",
"availableBalance": "可用余额",
"positionBalance": "仓位余额",
"totalBalance": "总余额",
+2 -1
View File
@@ -214,7 +214,7 @@
"walletType": "錢包類型",
"walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶",
"loadingProxyOptions": "正在獲取代理地址和資產信息...",
"selectProxyOption": "請選擇代理地址",
"selectProxyOption": "請選擇帳戶類型",
"proxyOptionRequired": "請選擇一個代理地址",
"proxyOption": {
"magic": {
@@ -226,6 +226,7 @@
"title": "Safe 代理地址"
},
"proxyAddress": "代理地址",
"proxyAddressHelp": "代理地址是您在 Polymarket 上實際使用的錢包地址:Magic 為郵箱/社群登入帳戶,Safe 為瀏覽器錢包(如 MetaMask)帳戶。請選擇與您 Polymarket 登入方式一致的一項。",
"availableBalance": "可用餘額",
"positionBalance": "倉位餘額",
"totalBalance": "總餘額",
+3 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input } from 'antd'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert } from 'antd'
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
@@ -883,9 +883,9 @@ const AccountList: React.FC = () => {
accountImportForm.resetFields()
}}
footer={null}
width={isMobile ? '95%' : 600}
width={isMobile ? '95%' : 640}
style={{ top: isMobile ? 20 : 50 }}
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 150px)', overflow: 'auto' }}
bodyStyle={{ padding: isMobile ? '16px 20px' : '24px 28px', maxHeight: 'calc(100vh - 140px)', overflow: 'auto' }}
destroyOnClose
maskClosable
closable
@@ -1106,9 +1106,9 @@ const AddModal: React.FC<AddModalProps> = ({
accountImportForm.resetFields()
}}
footer={null}
width={isMobile ? '95%' : 600}
width={isMobile ? '95%' : 640}
style={{ top: isMobile ? 20 : 50 }}
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 150px)', overflow: 'auto' }}
bodyStyle={{ padding: isMobile ? '16px 20px' : '24px 28px', maxHeight: 'calc(100vh - 140px)', overflow: 'auto' }}
destroyOnClose
maskClosable
closable
+1 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo, useRef } from 'react'
import { useEffect, useState, useMemo } 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'