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
co-authored by Cursor
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 { useState, useEffect } from 'react'
import { Form, Input, Button, Radio, Space, Card, Spin, message, Alert } from 'antd' import { Form, Input, Button, Radio, Space, Card, Spin, message, Alert, Steps, Tag } from 'antd'
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons' import { KeyOutlined, WalletOutlined, UserOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore' import { useAccountStore } from '../store/accountStore'
import { import {
@@ -42,9 +42,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
const [loadingProxyOptions, setLoadingProxyOptions] = useState<boolean>(false) const [loadingProxyOptions, setLoadingProxyOptions] = useState<boolean>(false)
const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址 const [step, setStep] = useState<'input' | 'select'>('input') // 步骤:输入 -> 选择代理地址
// 当私钥输入时,自动推导地址 // 当私钥输入时,自动推导地址(不支持换行,自动去除换行符)
const handlePrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { 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) { if (!privateKey) {
setDerivedAddress('') setDerivedAddress('')
setAddressError('') setAddressError('')
@@ -85,9 +90,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
} }
} }
// 当助记词输入时,自动推导地址 // 当助记词输入时,自动推导地址(不支持换行,换行符转为空格)
const handleMnemonicChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { 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) { if (!mnemonic) {
setDerivedAddress('') setDerivedAddress('')
setAddressError('') setAddressError('')
@@ -150,7 +160,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
if (options.length > 0) { if (options.length > 0) {
setStep('select') setStep('select')
// 如果有资产,默认选择第一个有资产的选项 // 如果有资产,默认选择第一个有资产的选项
const hasAssetsOption = options.find(opt => opt.hasAssets) const hasAssetsOption = options.find((opt: ProxyOption) => opt.hasAssets)
if (hasAssetsOption) { if (hasAssetsOption) {
setSelectedProxyType(hasAssetsOption.walletType) setSelectedProxyType(hasAssetsOption.walletType)
} else { } else {
@@ -262,23 +272,38 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
} }
} }
const currentStep = step === 'input' ? 0 : 1
return ( 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={form} form={form}
layout="vertical" layout="vertical"
onFinish={handleSubmit} onFinish={handleSubmit}
size={isMobile ? 'middle' : 'large'} size={isMobile ? 'middle' : 'large'}
> >
<Form.Item label={t('accountImport.importMethod')}> <Form.Item label={t('accountImport.importMethod')} style={{ marginBottom: 16 }}>
<Radio.Group <Radio.Group
value={importType} value={importType}
onChange={(e) => { onChange={(e) => {
setImportType(e.target.value) setImportType(e.target.value)
}} }}
optionType="button"
buttonStyle="solid"
size={isMobile ? 'middle' : 'large'}
> >
<Radio value="privateKey">{t('accountImport.privateKey')}</Radio> <Radio.Button value="privateKey">{t('accountImport.privateKey')}</Radio.Button>
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio> <Radio.Button value="mnemonic">{t('accountImport.mnemonic')}</Radio.Button>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
@@ -303,9 +328,10 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''} validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
> >
<Input.TextArea <Input.TextArea
rows={3} rows={2}
placeholder={t('accountImport.privateKeyPlaceholder')} placeholder={t('accountImport.privateKeyPlaceholder')}
onChange={handlePrivateKeyChange} onChange={handlePrivateKeyChange}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
disabled={loadingProxyOptions} disabled={loadingProxyOptions}
/> />
</Form.Item> </Form.Item>
@@ -357,9 +383,10 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''} validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={2}
placeholder={t('accountImport.mnemonicPlaceholder')} placeholder={t('accountImport.mnemonicPlaceholder')}
onChange={handleMnemonicChange} onChange={handleMnemonicChange}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
disabled={loadingProxyOptions} disabled={loadingProxyOptions}
/> />
</Form.Item> </Form.Item>
@@ -397,14 +424,14 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
<Form.Item> <Form.Item>
<Alert <Alert
message={ message={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Spin size="small" /> <Spin size="small" />
<span>{t('accountImport.loadingProxyOptions')}</span> <span>{t('accountImport.loadingProxyOptions')}</span>
</div> </div>
} }
type="info" type="info"
showIcon={false} showIcon={false}
style={{ marginBottom: '16px' }} style={{ marginBottom: 16 }}
/> />
</Form.Item> </Form.Item>
)} )}
@@ -424,70 +451,70 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
} }
} }
]} ]}
style={{ marginBottom: 20 }}
> >
{loadingProxyOptions ? ( {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"> <Space direction="vertical" style={{ width: '100%' }} size={12}>
{proxyOptions.map((option) => ( {proxyOptions.map((option) => {
<Card const isSelected = selectedProxyType === option.walletType
key={option.walletType} const typeLabel = option.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'
hoverable return (
onClick={() => setSelectedProxyType(option.walletType)} <Card
style={{ key={option.walletType}
cursor: 'pointer', hoverable
border: selectedProxyType === option.walletType ? '2px solid #1890ff' : '1px solid #d9d9d9', onClick={() => setSelectedProxyType(option.walletType)}
backgroundColor: selectedProxyType === option.walletType ? '#e6f7ff' : '#fff' size="small"
}} style={{
> cursor: 'pointer',
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> borderColor: isSelected ? 'var(--ant-color-primary)' : undefined,
<div style={{ flex: 1 }}> borderWidth: isSelected ? 2 : 1,
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}> backgroundColor: isSelected ? 'var(--ant-color-primary-bg)' : undefined,
<Radio checked={selectedProxyType === option.walletType} /> transition: 'border-color 0.2s, background-color 0.2s'
<strong style={{ marginLeft: '8px' }}> }}
{t(`accountImport.proxyOption.${option.walletType}.title`)} >
</strong> <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 && ( {option.hasAssets && (
<span style={{ marginLeft: '8px', color: '#52c41a' }}> <span style={{ color: '#52c41a', fontSize: 12 }}>
<CheckCircleOutlined /> {t('accountImport.proxyOption.hasAssets')} <CheckCircleOutlined /> {t('accountImport.proxyOption.hasAssets')}
</span> </span>
)} )}
{option.error && ( {option.error && (
<span style={{ marginLeft: '8px', color: '#ff4d4f' }}> <span style={{ color: 'var(--ant-color-error)', fontSize: 12 }}>
<ExclamationCircleOutlined /> {t('accountImport.proxyOption.error')} <ExclamationCircleOutlined /> {t('accountImport.proxyOption.error')}
</span> </span>
)} )}
</div> </Space>
<div style={{ marginLeft: '24px', color: '#666', fontSize: '14px', marginBottom: '8px' }}> {!option.error && (
{t(`accountImport.proxyOption.${option.walletType}.description`)} <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ant-color-primary)' }}>
</div> {formatUSDC(option.totalBalance)} USDC
<div style={{ marginLeft: '24px', fontSize: '12px', color: '#999', marginBottom: '4px' }}> </span>
{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>
</div> <div style={{ marginTop: 8, marginLeft: 28, fontSize: 12, color: 'var(--ant-color-text-secondary)', wordBreak: 'break-all' }}>
</Card> {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> </Space>
)} )}
</Form.Item> </Form.Item>
@@ -496,30 +523,32 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
<Form.Item <Form.Item
label={t('accountImport.accountName')} label={t('accountImport.accountName')}
name="accountName" name="accountName"
style={{ marginBottom: 24 }}
> >
<Input placeholder={t('accountImport.accountNamePlaceholder')} /> <Input placeholder={t('accountImport.accountNamePlaceholder')} />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item style={{ marginBottom: 0 }}>
<Space> <Space size="middle">
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
loading={loading} loading={loading}
disabled={step !== 'select' || !selectedProxyType || loadingProxyOptions} disabled={step !== 'select' || !selectedProxyType || loadingProxyOptions}
size={isMobile ? 'middle' : 'large'} size={isMobile ? 'middle' : 'large'}
style={isMobile ? { minHeight: 44 } : undefined}
> >
{t('accountImport.importAccount')} {t('accountImport.importAccount')}
</Button> </Button>
{showCancelButton && onCancel && ( {showCancelButton && onCancel && (
<Button onClick={onCancel}> <Button onClick={onCancel} size={isMobile ? 'middle' : 'large'}>
{t('common.cancel')} {t('common.cancel')}
</Button> </Button>
)} )}
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>
</> </div>
) )
} }
+2 -1
View File
@@ -214,7 +214,7 @@
"walletType": "Wallet Type", "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.)", "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...", "loadingProxyOptions": "Loading proxy addresses and asset information...",
"selectProxyOption": "Please select a proxy address", "selectProxyOption": "Please select account type",
"proxyOptionRequired": "Please select a proxy address", "proxyOptionRequired": "Please select a proxy address",
"proxyOption": { "proxyOption": {
"magic": { "magic": {
@@ -226,6 +226,7 @@
"title": "Safe Proxy Address" "title": "Safe Proxy Address"
}, },
"proxyAddress": "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", "availableBalance": "Available Balance",
"positionBalance": "Position Balance", "positionBalance": "Position Balance",
"totalBalance": "Total Balance", "totalBalance": "Total Balance",
+2 -1
View File
@@ -213,7 +213,7 @@
"walletType": "钱包类型", "walletType": "钱包类型",
"walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户", "walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户",
"loadingProxyOptions": "正在获取代理地址和资产信息...", "loadingProxyOptions": "正在获取代理地址和资产信息...",
"selectProxyOption": "请选择代理地址", "selectProxyOption": "请选择账户类型",
"proxyOptionRequired": "请选择一个代理地址", "proxyOptionRequired": "请选择一个代理地址",
"proxyOption": { "proxyOption": {
"magic": { "magic": {
@@ -225,6 +225,7 @@
"title": "Safe 代理地址" "title": "Safe 代理地址"
}, },
"proxyAddress": "代理地址", "proxyAddress": "代理地址",
"proxyAddressHelp": "代理地址是您在 Polymarket 上实际使用的钱包地址:Magic 为邮箱/社交登录账户,Safe 为浏览器钱包(如 MetaMask)账户。请选择与您 Polymarket 登录方式一致的一项。",
"availableBalance": "可用余额", "availableBalance": "可用余额",
"positionBalance": "仓位余额", "positionBalance": "仓位余额",
"totalBalance": "总余额", "totalBalance": "总余额",
+2 -1
View File
@@ -214,7 +214,7 @@
"walletType": "錢包類型", "walletType": "錢包類型",
"walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶", "walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶",
"loadingProxyOptions": "正在獲取代理地址和資產信息...", "loadingProxyOptions": "正在獲取代理地址和資產信息...",
"selectProxyOption": "請選擇代理地址", "selectProxyOption": "請選擇帳戶類型",
"proxyOptionRequired": "請選擇一個代理地址", "proxyOptionRequired": "請選擇一個代理地址",
"proxyOption": { "proxyOption": {
"magic": { "magic": {
@@ -226,6 +226,7 @@
"title": "Safe 代理地址" "title": "Safe 代理地址"
}, },
"proxyAddress": "代理地址", "proxyAddress": "代理地址",
"proxyAddressHelp": "代理地址是您在 Polymarket 上實際使用的錢包地址:Magic 為郵箱/社群登入帳戶,Safe 為瀏覽器錢包(如 MetaMask)帳戶。請選擇與您 Polymarket 登入方式一致的一項。",
"availableBalance": "可用餘額", "availableBalance": "可用餘額",
"positionBalance": "倉位餘額", "positionBalance": "倉位餘額",
"totalBalance": "總餘額", "totalBalance": "總餘額",
+3 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react' 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 { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore' import { useAccountStore } from '../store/accountStore'
@@ -883,9 +883,9 @@ const AccountList: React.FC = () => {
accountImportForm.resetFields() accountImportForm.resetFields()
}} }}
footer={null} footer={null}
width={isMobile ? '95%' : 600} width={isMobile ? '95%' : 640}
style={{ top: isMobile ? 20 : 50 }} 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 destroyOnClose
maskClosable maskClosable
closable closable
@@ -1106,9 +1106,9 @@ const AddModal: React.FC<AddModalProps> = ({
accountImportForm.resetFields() accountImportForm.resetFields()
}} }}
footer={null} footer={null}
width={isMobile ? '95%' : 600} width={isMobile ? '95%' : 640}
style={{ top: isMobile ? 20 : 50 }} 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 destroyOnClose
maskClosable maskClosable
closable 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 { 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 { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'