feat(frontend): 优化导入账户弹窗与文案
- 导入弹窗:增加步骤条、导入方式改为按钮、代理选项卡片紧凑展示 - 代理选项:标题改为「请选择账户类型」,地址完整显示,有资产标绿,小白说明放入卡片内 - 私钥/助记词:输入框默认两行、禁止换行(换行自动转空格/去除) - 多语言:新增 proxyAddressHelp,更新 selectProxyOption 文案 - Modal 宽度与内边距微调;PositionList 移除未使用 useRef Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "总余额",
|
||||
|
||||
@@ -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": "總餘額",
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
Reference in New Issue
Block a user