feat: 完成前端多语言支持和菜单优化
- 缩短菜单标题,解决显示不全问题 - 添加跟随系统语言选项,作为默认选项 - 移除语言切换时的页面刷新,实现无刷新切换 - 完成主要页面的多语言替换: * ConfigPage - 全局配置页面 * ResetPassword - 重置密码页面 * LeaderList - Leader 列表页面 * UserList - 用户列表页面 * Statistics - 统计信息页面 * OrderList - 订单列表页面 * TemplateList - 模板列表页面 * CopyTradingList - 跟单配置列表页面 - 添加简体中文、繁体中文和英文的完整翻译键 - 优化语言设置页面,支持跟随系统语言
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Card, Descriptions, Button, Space, Tag, Spin, message, Typography, Divider, Modal, Form, Input, Alert } from 'antd'
|
||||
import { ArrowLeftOutlined, ReloadOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
|
||||
const { Title } = Typography
|
||||
|
||||
const AccountDetail: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
@@ -29,7 +31,7 @@ const AccountDetail: React.FC = () => {
|
||||
loadAccountDetail()
|
||||
loadBalance()
|
||||
} else {
|
||||
message.error('账户ID不能为空')
|
||||
message.error(t('account.accountIdRequired'))
|
||||
navigate('/accounts')
|
||||
}
|
||||
}, [accountId])
|
||||
@@ -42,7 +44,7 @@ const AccountDetail: React.FC = () => {
|
||||
const accountData = await fetchAccountDetail(Number(accountId))
|
||||
setAccount(accountData)
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取账户详情失败')
|
||||
message.error(error.message || t('account.getDetailFailed'))
|
||||
navigate('/accounts')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -89,7 +91,7 @@ const AccountDetail: React.FC = () => {
|
||||
|
||||
await updateAccount(updateData)
|
||||
|
||||
message.success('更新账户成功')
|
||||
message.success(t('account.updateSuccess'))
|
||||
setEditModalVisible(false)
|
||||
editForm.resetFields()
|
||||
|
||||
@@ -98,7 +100,7 @@ const AccountDetail: React.FC = () => {
|
||||
await loadAccountDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '更新账户失败')
|
||||
message.error(error.message || t('account.updateFailed'))
|
||||
} finally {
|
||||
setEditLoading(false)
|
||||
}
|
||||
@@ -136,7 +138,7 @@ const AccountDetail: React.FC = () => {
|
||||
onClick={() => navigate('/accounts')}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
返回
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Title level={isMobile ? 4 : 2} style={{ margin: 0, fontSize: isMobile ? '16px' : undefined }}>
|
||||
{account.accountName || `账户 ${account.id}`}
|
||||
@@ -151,7 +153,7 @@ const AccountDetail: React.FC = () => {
|
||||
block={isMobile}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
刷新余额
|
||||
{t('account.refreshBalance')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -169,7 +171,7 @@ const AccountDetail: React.FC = () => {
|
||||
block={isMobile}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
编辑
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -184,13 +186,13 @@ const AccountDetail: React.FC = () => {
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ fontSize: isMobile ? '14px' : undefined }}
|
||||
>
|
||||
<Descriptions.Item label="账户ID">
|
||||
<Descriptions.Item label={t('account.accountId')}>
|
||||
{account.id}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户名称">
|
||||
<Descriptions.Item label={t('account.accountName')}>
|
||||
{account.accountName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
|
||||
<Descriptions.Item label={t('account.walletAddress')} span={isMobile ? 1 : 2}>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '14px',
|
||||
@@ -201,7 +203,7 @@ const AccountDetail: React.FC = () => {
|
||||
{account.walletAddress}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户余额">
|
||||
<Descriptions.Item label={t('account.balance')}>
|
||||
{balanceLoading ? (
|
||||
<Spin size="small" />
|
||||
) : balance ? (
|
||||
@@ -218,7 +220,7 @@ const AccountDetail: React.FC = () => {
|
||||
<Divider />
|
||||
|
||||
<Card
|
||||
title="API 凭证配置"
|
||||
title={t('account.apiCredentials')}
|
||||
style={{
|
||||
marginTop: isMobile ? '12px' : '16px',
|
||||
margin: isMobile ? '0 -8px' : '0',
|
||||
@@ -231,26 +233,26 @@ const AccountDetail: React.FC = () => {
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ fontSize: isMobile ? '14px' : undefined }}
|
||||
>
|
||||
<Descriptions.Item label="API Key">
|
||||
<Descriptions.Item label={t('account.apiKey')}>
|
||||
<Tag color={account.apiKeyConfigured ? 'success' : 'default'}>
|
||||
{account.apiKeyConfigured ? '已配置' : '未配置'}
|
||||
{account.apiKeyConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="API Secret">
|
||||
<Descriptions.Item label={t('account.apiSecret')}>
|
||||
<Tag color={account.apiSecretConfigured ? 'success' : 'default'}>
|
||||
{account.apiSecretConfigured ? '已配置' : '未配置'}
|
||||
{account.apiSecretConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="API Passphrase">
|
||||
<Descriptions.Item label={t('account.apiPassphrase')}>
|
||||
<Tag color={account.apiPassphraseConfigured ? 'success' : 'default'}>
|
||||
{account.apiPassphraseConfigured ? '已配置' : '未配置'}
|
||||
{account.apiPassphraseConfigured ? t('account.configured') : t('account.notConfigured')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="配置状态">
|
||||
<Descriptions.Item label={t('account.apiCredentials')}>
|
||||
{account.apiKeyConfigured && account.apiSecretConfigured && account.apiPassphraseConfigured ? (
|
||||
<Tag color="success">完整配置</Tag>
|
||||
<Tag color="success">{t('account.fullConfig')}</Tag>
|
||||
) : (
|
||||
<Tag color="warning">部分配置</Tag>
|
||||
<Tag color="warning">{t('account.partialConfig')}</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -262,7 +264,7 @@ const AccountDetail: React.FC = () => {
|
||||
<>
|
||||
<Divider style={{ margin: isMobile ? '12px 0' : '16px 0' }} />
|
||||
<Card
|
||||
title="交易统计"
|
||||
title={t('account.statistics')}
|
||||
style={{
|
||||
marginTop: isMobile ? '12px' : '16px',
|
||||
margin: isMobile ? '0 -8px' : '0',
|
||||
@@ -276,27 +278,27 @@ const AccountDetail: React.FC = () => {
|
||||
style={{ fontSize: isMobile ? '14px' : undefined }}
|
||||
>
|
||||
{account.totalOrders !== undefined && (
|
||||
<Descriptions.Item label="总订单数">
|
||||
<Descriptions.Item label={t('account.totalOrders')}>
|
||||
{account.totalOrders}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{account.activeOrders !== undefined && (
|
||||
<Descriptions.Item label="活跃订单数">
|
||||
<Descriptions.Item label={t('account.activeOrders')}>
|
||||
<Tag color={account.activeOrders > 0 ? 'orange' : 'default'}>{account.activeOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{account.completedOrders !== undefined && (
|
||||
<Descriptions.Item label="已完成订单数">
|
||||
<Descriptions.Item label={t('account.completedOrders')}>
|
||||
<Tag color="success">{account.completedOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{account.positionCount !== undefined && (
|
||||
<Descriptions.Item label="持仓数量">
|
||||
<Descriptions.Item label={t('account.positionCount')}>
|
||||
<Tag color={account.positionCount > 0 ? 'blue' : 'default'}>{account.positionCount}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{account.totalPnl !== undefined && (
|
||||
<Descriptions.Item label="总盈亏">
|
||||
<Descriptions.Item label={t('account.totalPnl')}>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: account.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
||||
@@ -312,7 +314,7 @@ const AccountDetail: React.FC = () => {
|
||||
|
||||
{/* 编辑账户 Modal */}
|
||||
<Modal
|
||||
title={account ? `编辑账户 - ${account.accountName || `账户 ${account.id}`}` : '编辑账户'}
|
||||
title={account ? `${t('common.edit')} ${t('account.title')} - ${account.accountName || `${t('account.title')} ${account.id}`}` : t('common.edit') + ' ' + t('account.title')}
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false)
|
||||
@@ -333,42 +335,42 @@ const AccountDetail: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Alert
|
||||
message="编辑提示"
|
||||
description="API 凭证字段留空表示不修改。如需更新 API 凭证,请输入新值;如需保持原值不变,请留空。"
|
||||
message={t('account.editTip')}
|
||||
description={t('account.editTipDesc')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
label="账户名称"
|
||||
label={t('account.accountName')}
|
||||
name="accountName"
|
||||
>
|
||||
<Input placeholder="账户名称(可选)" />
|
||||
<Input placeholder={t('account.accountNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Key"
|
||||
label={t('account.apiKey')}
|
||||
name="apiKey"
|
||||
help="留空表示不修改,输入新值将更新 API Key"
|
||||
help={t('account.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Secret"
|
||||
label={t('account.apiSecret')}
|
||||
name="apiSecret"
|
||||
help="留空表示不修改,输入新值将更新 API Secret"
|
||||
help={t('account.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Passphrase"
|
||||
label={t('account.apiPassphrase')}
|
||||
name="apiPassphrase"
|
||||
help="留空表示不修改,输入新值将更新 API Passphrase"
|
||||
help={t('account.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('account.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@@ -381,7 +383,7 @@ const AccountDetail: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -390,7 +392,7 @@ const AccountDetail: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
@@ -398,7 +400,7 @@ const AccountDetail: React.FC = () => {
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: '16px' }}>加载中...</div>
|
||||
<div style={{ marginTop: '16px' }}>{t('common.loading')}</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
@@ -408,3 +410,7 @@ const AccountDetail: React.FC = () => {
|
||||
|
||||
export default AccountDetail
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography, Radio, Space, Alert } from 'antd'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import {
|
||||
getAddressFromPrivateKey,
|
||||
@@ -18,6 +19,7 @@ const { Title } = Typography
|
||||
type ImportType = 'privateKey' | 'mnemonic'
|
||||
|
||||
const AccountImport: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { importAccount, loading } = useAccountStore()
|
||||
@@ -37,7 +39,7 @@ const AccountImport: React.FC = () => {
|
||||
|
||||
// 验证私钥格式
|
||||
if (!isValidPrivateKey(privateKey)) {
|
||||
setAddressError('私钥格式不正确(应为64位十六进制字符串)')
|
||||
setAddressError(t('accountImport.privateKeyInvalid'))
|
||||
setDerivedAddress('')
|
||||
return
|
||||
}
|
||||
@@ -50,7 +52,7 @@ const AccountImport: React.FC = () => {
|
||||
// 自动填充钱包地址字段
|
||||
form.setFieldsValue({ walletAddress: address })
|
||||
} catch (error: any) {
|
||||
setAddressError(error.message || '无法从私钥推导地址')
|
||||
setAddressError(error.message || t('accountImport.addressError'))
|
||||
setDerivedAddress('')
|
||||
}
|
||||
}
|
||||
@@ -66,7 +68,7 @@ const AccountImport: React.FC = () => {
|
||||
|
||||
// 验证助记词格式
|
||||
if (!isValidMnemonic(mnemonic)) {
|
||||
setAddressError('助记词格式不正确(应为12或24个单词,用空格分隔)')
|
||||
setAddressError(t('accountImport.mnemonicInvalid'))
|
||||
setDerivedAddress('')
|
||||
return
|
||||
}
|
||||
@@ -79,7 +81,7 @@ const AccountImport: React.FC = () => {
|
||||
// 自动填充钱包地址字段
|
||||
form.setFieldsValue({ walletAddress: address })
|
||||
} catch (error: any) {
|
||||
setAddressError(error.message || '无法从助记词推导地址')
|
||||
setAddressError(error.message || t('accountImport.addressErrorMnemonic'))
|
||||
setDerivedAddress('')
|
||||
}
|
||||
}
|
||||
@@ -96,13 +98,13 @@ const AccountImport: React.FC = () => {
|
||||
|
||||
// 验证推导的地址和输入的地址是否一致
|
||||
if (derivedAddress && walletAddress !== derivedAddress) {
|
||||
message.error('钱包地址与私钥不匹配')
|
||||
message.error(t('accountImport.walletAddressMismatch'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 助记词模式
|
||||
if (!values.mnemonic) {
|
||||
message.error('请输入助记词')
|
||||
message.error(t('accountImport.mnemonicRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,7 +116,7 @@ const AccountImport: React.FC = () => {
|
||||
if (values.walletAddress) {
|
||||
if (values.walletAddress !== derivedAddressFromMnemonic) {
|
||||
// 地址不匹配,使用推导的地址(因为私钥是从助记词导出的,必须使用对应的地址)
|
||||
message.warning(`输入的地址与助记词推导的地址不一致。推导的地址: ${derivedAddressFromMnemonic},将使用推导的地址`)
|
||||
message.warning(`${t('accountImport.walletAddressMismatchMnemonic')}: ${derivedAddressFromMnemonic}`)
|
||||
walletAddress = derivedAddressFromMnemonic
|
||||
} else {
|
||||
// 地址匹配,使用用户输入的地址
|
||||
@@ -128,7 +130,7 @@ const AccountImport: React.FC = () => {
|
||||
|
||||
// 验证钱包地址格式
|
||||
if (!isValidWalletAddress(walletAddress)) {
|
||||
message.error('钱包地址格式不正确')
|
||||
message.error(t('accountImport.walletAddressInvalid'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,10 +140,10 @@ const AccountImport: React.FC = () => {
|
||||
accountName: values.accountName
|
||||
})
|
||||
|
||||
message.success('导入账户成功')
|
||||
message.success(t('accountImport.importSuccess'))
|
||||
navigate('/accounts')
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '导入账户失败')
|
||||
message.error(error.message || t('accountImport.importFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,15 +155,15 @@ const AccountImport: React.FC = () => {
|
||||
onClick={() => navigate('/accounts')}
|
||||
style={{ marginBottom: '16px' }}
|
||||
>
|
||||
返回
|
||||
{t('accountImport.back')}
|
||||
</Button>
|
||||
<Title level={2} style={{ margin: 0 }}>导入账户</Title>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('accountImport.title')}</Title>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Alert
|
||||
message="安全提示"
|
||||
description="私钥将存储在后端数据库中,请确保数据库访问安全。建议使用 HTTPS 连接。"
|
||||
message={t('accountImport.securityTip')}
|
||||
description={t('accountImport.securityTipDesc')}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
@@ -173,7 +175,7 @@ const AccountImport: React.FC = () => {
|
||||
onFinish={handleSubmit}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Form.Item label="导入方式">
|
||||
<Form.Item label={t('accountImport.importMethod')}>
|
||||
<Radio.Group
|
||||
value={importType}
|
||||
onChange={(e) => {
|
||||
@@ -183,51 +185,51 @@ const AccountImport: React.FC = () => {
|
||||
form.setFieldsValue({ walletAddress: '' })
|
||||
}}
|
||||
>
|
||||
<Radio value="privateKey">私钥</Radio>
|
||||
<Radio value="mnemonic">助记词</Radio>
|
||||
<Radio value="privateKey">{t('accountImport.privateKey')}</Radio>
|
||||
<Radio value="mnemonic">{t('accountImport.mnemonic')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{importType === 'privateKey' ? (
|
||||
<>
|
||||
<Form.Item
|
||||
label="私钥"
|
||||
label={t('accountImport.privateKeyLabel')}
|
||||
name="privateKey"
|
||||
rules={[
|
||||
{ required: true, message: '请输入私钥' },
|
||||
{ required: true, message: t('accountImport.privateKeyRequired') },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!isValidPrivateKey(value)) {
|
||||
return Promise.reject(new Error('私钥格式不正确(应为64位十六进制字符串)'))
|
||||
return Promise.reject(new Error(t('accountImport.privateKeyInvalid')))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
|
||||
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
|
||||
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入私钥(64位十六进制字符串,可选0x前缀)"
|
||||
placeholder={t('accountImport.privateKeyPlaceholder')}
|
||||
onChange={handlePrivateKeyChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="钱包地址"
|
||||
label={t('accountImport.walletAddress')}
|
||||
name="walletAddress"
|
||||
rules={[
|
||||
{ required: true, message: '请输入钱包地址' },
|
||||
{ required: true, message: t('accountImport.walletAddressRequired') },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!isValidWalletAddress(value)) {
|
||||
return Promise.reject(new Error('钱包地址格式不正确'))
|
||||
return Promise.reject(new Error(t('accountImport.walletAddressInvalid')))
|
||||
}
|
||||
if (derivedAddress && value !== derivedAddress) {
|
||||
return Promise.reject(new Error('钱包地址与私钥不匹配'))
|
||||
return Promise.reject(new Error(t('accountImport.walletAddressMismatch')))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -235,7 +237,7 @@ const AccountImport: React.FC = () => {
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="钱包地址(将从私钥自动推导)"
|
||||
placeholder={t('accountImport.walletAddressPlaceholder')}
|
||||
readOnly={!!derivedAddress}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -243,43 +245,43 @@ const AccountImport: React.FC = () => {
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
label="助记词"
|
||||
label={t('accountImport.mnemonicLabel')}
|
||||
name="mnemonic"
|
||||
rules={[
|
||||
{ required: true, message: '请输入助记词' },
|
||||
{ required: true, message: t('accountImport.mnemonicRequired') },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!isValidMnemonic(value)) {
|
||||
return Promise.reject(new Error('助记词格式不正确(应为12或24个单词,用空格分隔)'))
|
||||
return Promise.reject(new Error(t('accountImport.mnemonicInvalid')))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
|
||||
help={addressError || (derivedAddress ? `${t('accountImport.derivedAddress')}: ${derivedAddress}` : '')}
|
||||
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="请输入12或24个单词的助记词(用空格分隔)"
|
||||
placeholder={t('accountImport.mnemonicPlaceholder')}
|
||||
onChange={handleMnemonicChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="钱包地址"
|
||||
label={t('accountImport.walletAddress')}
|
||||
name="walletAddress"
|
||||
rules={[
|
||||
{ required: true, message: '请输入钱包地址' },
|
||||
{ required: true, message: t('accountImport.walletAddressRequired') },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!isValidWalletAddress(value)) {
|
||||
return Promise.reject(new Error('钱包地址格式不正确'))
|
||||
return Promise.reject(new Error(t('accountImport.walletAddressInvalid')))
|
||||
}
|
||||
if (derivedAddress && value !== derivedAddress) {
|
||||
return Promise.reject(new Error('钱包地址与助记词不匹配'))
|
||||
return Promise.reject(new Error(t('accountImport.walletAddressMismatchMnemonic')))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -287,7 +289,7 @@ const AccountImport: React.FC = () => {
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="钱包地址(将从助记词自动推导)"
|
||||
placeholder={t('accountImport.walletAddressPlaceholder')}
|
||||
readOnly={!!derivedAddress}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -295,10 +297,10 @@ const AccountImport: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label="账户名称"
|
||||
label={t('accountImport.accountName')}
|
||||
name="accountName"
|
||||
>
|
||||
<Input placeholder="可选,用于标识账户" />
|
||||
<Input placeholder={t('accountImport.accountNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -310,10 +312,10 @@ const AccountImport: React.FC = () => {
|
||||
loading={loading}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
导入账户
|
||||
{t('accountImport.importAccount')}
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/accounts')}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
+105
-103
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
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'
|
||||
import type { Account } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
|
||||
const { Title } = Typography
|
||||
|
||||
const AccountList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, loading, fetchAccounts, deleteAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
|
||||
@@ -65,17 +67,17 @@ const AccountList: React.FC = () => {
|
||||
const handleDelete = async (account: Account) => {
|
||||
try {
|
||||
await deleteAccount(account.id)
|
||||
message.success('删除账户成功')
|
||||
message.success(t('accountList.deleteSuccess'))
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除账户失败')
|
||||
message.error(error.message || t('accountList.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopy = (text: string, label: string) => {
|
||||
const handleCopy = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success(`${label}已复制到剪贴板`)
|
||||
message.success(t('accountList.copySuccess'))
|
||||
}).catch(() => {
|
||||
message.error('复制失败')
|
||||
message.error(t('accountList.copyFailed'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,13 +111,13 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取账户详情失败:', error)
|
||||
message.error(error.message || '获取账户详情失败')
|
||||
message.error(error.message || t('accountList.getDetailFailed'))
|
||||
setDetailModalVisible(false)
|
||||
setDetailAccount(null)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('打开详情失败:', error)
|
||||
message.error('打开详情失败')
|
||||
message.error(t('accountList.openDetailFailed'))
|
||||
setDetailModalVisible(false)
|
||||
setDetailAccount(null)
|
||||
}
|
||||
@@ -133,9 +135,9 @@ const AccountList: React.FC = () => {
|
||||
position: balanceData.positionBalance || '0',
|
||||
positions: balanceData.positions || []
|
||||
})
|
||||
message.success('余额刷新成功')
|
||||
message.success(t('accountList.refreshBalanceSuccess'))
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '刷新余额失败')
|
||||
message.error(error.message || t('accountList.refreshBalanceFailed'))
|
||||
} finally {
|
||||
setDetailBalanceLoading(false)
|
||||
}
|
||||
@@ -158,7 +160,7 @@ const AccountList: React.FC = () => {
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('打开编辑失败:', error)
|
||||
message.error(error.message || '获取账户详情失败')
|
||||
message.error(error.message || t('accountList.getDetailFailedForEdit'))
|
||||
setEditModalVisible(false)
|
||||
setEditAccount(null)
|
||||
}
|
||||
@@ -188,7 +190,7 @@ const AccountList: React.FC = () => {
|
||||
|
||||
await updateAccount(updateData)
|
||||
|
||||
message.success('更新账户成功')
|
||||
message.success(t('accountList.updateSuccess'))
|
||||
setEditModalVisible(false)
|
||||
setEditAccount(null)
|
||||
editForm.resetFields()
|
||||
@@ -202,7 +204,7 @@ const AccountList: React.FC = () => {
|
||||
setDetailAccount(accountDetail)
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '更新账户失败')
|
||||
message.error(error.message || t('accountList.updateFailed'))
|
||||
} finally {
|
||||
setEditLoading(false)
|
||||
}
|
||||
@@ -210,13 +212,13 @@ const AccountList: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '账户名称',
|
||||
title: t('accountList.accountName'),
|
||||
dataIndex: 'accountName',
|
||||
key: 'accountName',
|
||||
render: (text: string, record: Account) => text || `账户 ${record.id}`
|
||||
render: (text: string, record: Account) => text || `${t('accountList.accountName')} ${record.id}`
|
||||
},
|
||||
{
|
||||
title: '钱包地址',
|
||||
title: t('accountList.walletAddress'),
|
||||
dataIndex: 'walletAddress',
|
||||
key: 'walletAddress',
|
||||
render: (text: string) => (
|
||||
@@ -226,14 +228,14 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(text, '钱包地址')}
|
||||
title="复制钱包地址"
|
||||
onClick={() => handleCopy(text)}
|
||||
title={t('accountList.walletAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '代理钱包地址',
|
||||
title: t('accountList.proxyAddress'),
|
||||
dataIndex: 'proxyAddress',
|
||||
key: 'proxyAddress',
|
||||
render: (address: string) => (
|
||||
@@ -243,27 +245,27 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(address, '代理钱包地址')}
|
||||
title="复制代理钱包地址"
|
||||
onClick={() => handleCopy(address)}
|
||||
title={t('accountList.proxyAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'API 凭证',
|
||||
title: t('accountList.apiCredentials'),
|
||||
key: 'apiCredentials',
|
||||
render: (_: any, record: Account) => {
|
||||
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
|
||||
const partialConfigured = record.apiKeyConfigured || record.apiSecretConfigured || record.apiPassphraseConfigured
|
||||
return (
|
||||
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'}>
|
||||
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
|
||||
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
title: t('accountList.balance'),
|
||||
dataIndex: 'balance',
|
||||
key: 'balance',
|
||||
render: (_: any, record: Account) => {
|
||||
@@ -276,7 +278,7 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '活跃订单',
|
||||
title: t('accountList.activeOrders'),
|
||||
dataIndex: 'activeOrders',
|
||||
key: 'activeOrders',
|
||||
render: (_: any, record: Account) => {
|
||||
@@ -287,7 +289,7 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('accountList.action'),
|
||||
key: 'action',
|
||||
render: (_: any, record: Account) => (
|
||||
<Space size="small">
|
||||
@@ -296,7 +298,7 @@ const AccountList: React.FC = () => {
|
||||
size="small"
|
||||
onClick={() => handleShowDetail(record)}
|
||||
>
|
||||
详情
|
||||
{t('accountList.detail')}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -304,22 +306,22 @@ const AccountList: React.FC = () => {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleShowEdit(record)}
|
||||
>
|
||||
编辑
|
||||
{t('accountList.edit')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个账户吗?"
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
? "删除账户前,请确保已取消所有活跃订单。删除后无法恢复,请谨慎操作!"
|
||||
: "删除后无法恢复,请谨慎操作!"
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定删除"
|
||||
cancelText="取消"
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
{t('accountList.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -329,7 +331,7 @@ const AccountList: React.FC = () => {
|
||||
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: '账户信息',
|
||||
title: t('accountList.accountName'),
|
||||
key: 'info',
|
||||
render: (_: any, record: Account) => {
|
||||
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
|
||||
@@ -342,7 +344,7 @@ const AccountList: React.FC = () => {
|
||||
marginBottom: '8px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{record.accountName || `账户 ${record.id}`}
|
||||
{record.accountName || `${t('accountList.accountName')} ${record.id}`}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
@@ -353,29 +355,29 @@ const AccountList: React.FC = () => {
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>钱包地址:</strong> {record.walletAddress}
|
||||
<strong>{t('accountList.walletAddress')}:</strong> {record.walletAddress}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.walletAddress, '钱包地址')}
|
||||
onClick={() => handleCopy(record.walletAddress)}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>代理钱包:</strong> {record.proxyAddress}
|
||||
<strong>{t('accountList.proxyAddress')}:</strong> {record.proxyAddress}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.proxyAddress, '代理钱包地址')}
|
||||
onClick={() => handleCopy(record.proxyAddress)}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'} style={{ margin: 0 }}>
|
||||
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
|
||||
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
|
||||
</Tag>
|
||||
</div>
|
||||
<div style={{
|
||||
@@ -383,7 +385,7 @@ const AccountList: React.FC = () => {
|
||||
fontWeight: '500',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
总余额: {balanceLoading[record.id] ? (
|
||||
{t('accountList.totalBalance')}: {balanceLoading[record.id] ? (
|
||||
<Spin size="small" style={{ marginLeft: '4px' }} />
|
||||
) : balanceMap[record.id]?.total && balanceMap[record.id].total !== '-' ? (
|
||||
`${formatUSDC(balanceMap[record.id].total)} USDC`
|
||||
@@ -397,7 +399,7 @@ const AccountList: React.FC = () => {
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
可用: {formatUSDC(balanceMap[record.id].available)} USDC | 仓位: {formatUSDC(balanceMap[record.id].position)} USDC
|
||||
{t('accountList.available')}: {formatUSDC(balanceMap[record.id].available)} USDC | {t('accountList.position')}: {formatUSDC(balanceMap[record.id].position)} USDC
|
||||
</div>
|
||||
)}
|
||||
{(record.activeOrders !== undefined && record.activeOrders !== null) && (
|
||||
@@ -409,7 +411,7 @@ const AccountList: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
活跃订单: <Tag color={record.activeOrders > 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders}</Tag>
|
||||
{t('accountList.activeOrders')}: <Tag color={record.activeOrders > 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders}</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -417,7 +419,7 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('accountList.action'),
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_: any, record: Account) => (
|
||||
@@ -429,7 +431,7 @@ const AccountList: React.FC = () => {
|
||||
onClick={() => handleShowDetail(record)}
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
查看详情
|
||||
{t('accountList.viewDetail')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -438,18 +440,18 @@ const AccountList: React.FC = () => {
|
||||
onClick={() => handleShowEdit(record)}
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
编辑
|
||||
{t('accountList.edit')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个账户吗?"
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
? "删除账户前,请确保已取消所有活跃订单。删除后无法恢复,请谨慎操作!"
|
||||
: "删除后无法恢复,请谨慎操作!"
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定删除"
|
||||
cancelText="取消"
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
@@ -458,7 +460,7 @@ const AccountList: React.FC = () => {
|
||||
danger
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
删除
|
||||
{t('accountList.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -481,7 +483,7 @@ const AccountList: React.FC = () => {
|
||||
padding: isMobile ? '0 8px' : '0'
|
||||
}}>
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, fontSize: isMobile ? '18px' : undefined }}>
|
||||
账户管理
|
||||
{t('accountList.title')}
|
||||
</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -491,7 +493,7 @@ const AccountList: React.FC = () => {
|
||||
block={isMobile}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
导入账户
|
||||
{t('accountList.importAccount')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -531,7 +533,7 @@ const AccountList: React.FC = () => {
|
||||
|
||||
{/* 账户详情 Modal */}
|
||||
<Modal
|
||||
title={detailAccount ? (detailAccount.accountName || `账户 ${detailAccount.id}`) : '账户详情'}
|
||||
title={detailAccount ? (detailAccount.accountName || `${t('accountList.accountName')} ${detailAccount.id}`) : t('accountList.accountDetail')}
|
||||
open={detailModalVisible}
|
||||
onCancel={() => {
|
||||
setDetailModalVisible(false)
|
||||
@@ -546,7 +548,7 @@ const AccountList: React.FC = () => {
|
||||
loading={detailBalanceLoading}
|
||||
disabled={!detailAccount}
|
||||
>
|
||||
刷新余额
|
||||
{t('accountList.refreshBalance')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="edit"
|
||||
@@ -560,7 +562,7 @@ const AccountList: React.FC = () => {
|
||||
}}
|
||||
disabled={!detailAccount}
|
||||
>
|
||||
编辑
|
||||
{t('accountList.edit')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="close"
|
||||
@@ -570,7 +572,7 @@ const AccountList: React.FC = () => {
|
||||
setDetailBalance(null)
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}
|
||||
width={isMobile ? '95%' : 800}
|
||||
@@ -586,13 +588,13 @@ const AccountList: React.FC = () => {
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
<Descriptions.Item label="账户ID">
|
||||
<Descriptions.Item label={t('accountList.accountId')}>
|
||||
{detailAccount.id}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户名称">
|
||||
<Descriptions.Item label={t('accountList.accountName')}>
|
||||
{detailAccount.accountName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
|
||||
<Descriptions.Item label={t('accountList.walletAddress')} span={isMobile ? 1 : 2}>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
@@ -607,12 +609,12 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.walletAddress || '', '钱包地址')}
|
||||
title="复制钱包地址"
|
||||
onClick={() => handleCopy(detailAccount.walletAddress || '')}
|
||||
title={t('accountList.walletAddress')}
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="代理钱包地址" span={isMobile ? 1 : 2}>
|
||||
<Descriptions.Item label={t('accountList.proxyAddress')} span={isMobile ? 1 : 2}>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
@@ -627,12 +629,12 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.proxyAddress || '', '代理钱包地址')}
|
||||
title="复制代理钱包地址"
|
||||
onClick={() => handleCopy(detailAccount.proxyAddress || '')}
|
||||
title={t('accountList.proxyAddress')}
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总余额" span={isMobile ? 1 : 2}>
|
||||
<Descriptions.Item label={t('accountList.totalBalance')} span={isMobile ? 1 : 2}>
|
||||
{detailBalanceLoading ? (
|
||||
<Spin size="small" />
|
||||
) : detailBalance ? (
|
||||
@@ -643,7 +645,7 @@ const AccountList: React.FC = () => {
|
||||
<span style={{ color: '#999' }}>-</span>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="可用余额">
|
||||
<Descriptions.Item label={t('accountList.available')}>
|
||||
{detailBalanceLoading ? (
|
||||
<Spin size="small" />
|
||||
) : detailBalance ? (
|
||||
@@ -654,7 +656,7 @@ const AccountList: React.FC = () => {
|
||||
<span style={{ color: '#999' }}>-</span>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="仓位余额">
|
||||
<Descriptions.Item label={t('accountList.position')}>
|
||||
{detailBalanceLoading ? (
|
||||
<Spin size="small" />
|
||||
) : detailBalance ? (
|
||||
@@ -673,28 +675,28 @@ const AccountList: React.FC = () => {
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
title="API 凭证配置"
|
||||
title={t('accountList.apiCredentials')}
|
||||
>
|
||||
<Descriptions.Item label="API Key">
|
||||
<Descriptions.Item label={t('accountList.apiKey')}>
|
||||
<Tag color={detailAccount.apiKeyConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiKeyConfigured ? '已配置' : '未配置'}
|
||||
{detailAccount.apiKeyConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="API Secret">
|
||||
<Descriptions.Item label={t('accountList.apiSecret')}>
|
||||
<Tag color={detailAccount.apiSecretConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiSecretConfigured ? '已配置' : '未配置'}
|
||||
{detailAccount.apiSecretConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="API Passphrase">
|
||||
<Descriptions.Item label={t('accountList.apiPassphrase')}>
|
||||
<Tag color={detailAccount.apiPassphraseConfigured ? 'success' : 'default'}>
|
||||
{detailAccount.apiPassphraseConfigured ? '已配置' : '未配置'}
|
||||
{detailAccount.apiPassphraseConfigured ? t('accountList.configured') : t('accountList.notConfiguredStatus')}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="配置状态">
|
||||
<Descriptions.Item label={t('accountList.configStatus')}>
|
||||
{detailAccount.apiKeyConfigured && detailAccount.apiSecretConfigured && detailAccount.apiPassphraseConfigured ? (
|
||||
<Tag color="success">完整配置</Tag>
|
||||
<Tag color="success">{t('accountList.fullConfig')}</Tag>
|
||||
) : (
|
||||
<Tag color="warning">部分配置</Tag>
|
||||
<Tag color="warning">{t('accountList.partialConfig')}</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -708,30 +710,30 @@ const AccountList: React.FC = () => {
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
title="交易统计"
|
||||
title={t('accountList.statistics')}
|
||||
>
|
||||
{detailAccount.totalOrders !== undefined && (
|
||||
<Descriptions.Item label="总订单数">
|
||||
<Descriptions.Item label={t('accountList.totalOrders')}>
|
||||
{detailAccount.totalOrders}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.activeOrders !== undefined && (
|
||||
<Descriptions.Item label="活跃订单数">
|
||||
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
|
||||
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.completedOrders !== undefined && (
|
||||
<Descriptions.Item label="已完成订单数">
|
||||
<Descriptions.Item label={t('accountList.completedOrders')}>
|
||||
<Tag color="success">{detailAccount.completedOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.positionCount !== undefined && (
|
||||
<Descriptions.Item label="持仓数量">
|
||||
<Descriptions.Item label={t('accountList.positionCount')}>
|
||||
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.totalPnl !== undefined && (
|
||||
<Descriptions.Item label="总盈亏">
|
||||
<Descriptions.Item label={t('accountList.totalPnl')}>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
||||
@@ -747,14 +749,14 @@ const AccountList: React.FC = () => {
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: '16px' }}>加载中...</div>
|
||||
<div style={{ marginTop: '16px' }}>{t('accountList.loading')}</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 编辑账户 Modal */}
|
||||
<Modal
|
||||
title={editAccount ? `编辑账户 - ${editAccount.accountName || `账户 ${editAccount.id}`}` : '编辑账户'}
|
||||
title={editAccount ? `${t('accountList.editAccount')} - ${editAccount.accountName || `${t('accountList.accountName')} ${editAccount.id}`}` : t('accountList.editAccount')}
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false)
|
||||
@@ -776,42 +778,42 @@ const AccountList: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Alert
|
||||
message="编辑提示"
|
||||
description="API 凭证字段留空表示不修改。如需更新 API 凭证,请输入新值;如需保持原值不变,请留空。"
|
||||
message={t('accountList.editTip')}
|
||||
description={t('accountList.editTipDesc')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
label="账户名称"
|
||||
label={t('accountList.accountName')}
|
||||
name="accountName"
|
||||
>
|
||||
<Input placeholder="账户名称(可选)" />
|
||||
<Input placeholder={t('accountList.accountNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Key"
|
||||
label={t('accountList.apiKey')}
|
||||
name="apiKey"
|
||||
help="留空表示不修改,输入新值将更新 API Key"
|
||||
help={t('accountList.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Secret"
|
||||
label={t('accountList.apiSecret')}
|
||||
name="apiSecret"
|
||||
help="留空表示不修改,输入新值将更新 API Secret"
|
||||
help={t('accountList.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="API Passphrase"
|
||||
label={t('accountList.apiPassphrase')}
|
||||
name="apiPassphrase"
|
||||
help="留空表示不修改,输入新值将更新 API Passphrase"
|
||||
help={t('accountList.leaveEmptyToNotModify')}
|
||||
>
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
<Input.Password placeholder={t('accountList.leaveEmptyToNotModify')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@@ -825,7 +827,7 @@ const AccountList: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -834,7 +836,7 @@ const AccountList: React.FC = () => {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
@@ -842,7 +844,7 @@ const AccountList: React.FC = () => {
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: '16px' }}>加载中...</div>
|
||||
<div style={{ marginTop: '16px' }}>{t('accountList.loading')}</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Button, Typography, Space, Badge, Spin, Row, Col } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface ApiHealthStatus {
|
||||
name: string
|
||||
url: string
|
||||
status: string
|
||||
message: string
|
||||
responseTime?: number
|
||||
}
|
||||
|
||||
const ApiHealthStatus: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [apiHealthStatus, setApiHealthStatus] = useState<ApiHealthStatus[]>([])
|
||||
const [checkingApiHealth, setCheckingApiHealth] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
checkApiHealth()
|
||||
}, [])
|
||||
|
||||
const checkApiHealth = async () => {
|
||||
setCheckingApiHealth(true)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.checkApiHealth()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setApiHealthStatus(response.data.data.apis)
|
||||
} else {
|
||||
// message.error(response.data.msg || 'API 健康检查失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
// message.error(error.message || 'API 健康检查失败')
|
||||
} finally {
|
||||
setCheckingApiHealth(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
if (status === 'success') {
|
||||
return '#52c41a'
|
||||
} else if (status === 'skipped') {
|
||||
return '#999'
|
||||
} else {
|
||||
return '#ff4d4f'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
if (status === 'success') {
|
||||
return t('apiHealthStatus.normal') || '正常'
|
||||
} else if (status === 'skipped') {
|
||||
return t('apiHealthStatus.notConfigured') || '未配置'
|
||||
} else {
|
||||
return t('apiHealthStatus.abnormal') || '异常'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('apiHealthStatus.title') || 'API 健康状态'}</Title>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={checkApiHealth}
|
||||
loading={checkingApiHealth}
|
||||
size="small"
|
||||
>
|
||||
{t('common.refresh') || '刷新'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Spin spinning={checkingApiHealth}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{apiHealthStatus.map((item, index) => (
|
||||
<Col
|
||||
key={index}
|
||||
xs={24}
|
||||
sm={12}
|
||||
md={12}
|
||||
lg={8}
|
||||
xl={6}
|
||||
>
|
||||
{isMobile ? (
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderLeft: `4px solid ${getStatusColor(item.status)}`,
|
||||
}}
|
||||
bodyStyle={{ padding: '12px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Space>
|
||||
{item.responseTime !== undefined && item.responseTime !== null && (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
|
||||
</Text>
|
||||
)}
|
||||
<Badge
|
||||
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderLeft: `4px solid ${getStatusColor(item.status)}`,
|
||||
height: '100%'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Badge
|
||||
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
|
||||
text={getStatusText(item.status)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px', wordBreak: 'break-all' }}>
|
||||
{item.url}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{item.message && item.message !== '连接成功' && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text
|
||||
type={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'secondary' : 'danger'}
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{item.message}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.responseTime !== undefined && item.responseTime !== null && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('apiHealthStatus.responseTime') || '响应时间'}: <Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Spin>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiHealthStatus
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Card, Typography, Alert } from 'antd'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
@@ -9,24 +10,26 @@ const { Title } = Typography
|
||||
* 请使用"跟单模板"和"跟单配置"页面进行配置
|
||||
*/
|
||||
const ConfigPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>全局配置</Title>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('configPage.title') || '全局配置'}</Title>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Alert
|
||||
message="配置功能已迁移"
|
||||
message={t('configPage.message') || '配置功能已迁移'}
|
||||
description={
|
||||
<div>
|
||||
<p>全局配置功能已迁移到以下页面:</p>
|
||||
<p>{t('configPage.description') || '全局配置功能已迁移到以下页面:'}</p>
|
||||
<ul>
|
||||
<li><strong>跟单模板</strong>:管理跟单参数(比例、金额、风险控制等)</li>
|
||||
<li><strong>跟单配置</strong>:将账户、模板和 Leader 关联,启用跟单关系</li>
|
||||
<li><strong>系统管理</strong>:配置代理、查看 API 健康状态</li>
|
||||
<li><strong>{t('configPage.templates') || '跟单模板'}</strong>:{t('configPage.templatesDesc') || '管理跟单参数(比例、金额、风险控制等)'}</li>
|
||||
<li><strong>{t('configPage.copyTrading') || '跟单配置'}</strong>:{t('configPage.copyTradingDesc') || '将账户、模板和 Leader 关联,启用跟单关系'}</li>
|
||||
<li><strong>{t('configPage.systemSettings') || '系统管理'}</strong>:{t('configPage.systemSettingsDesc') || '配置代理、查看 API 健康状态'}</li>
|
||||
</ul>
|
||||
<p>请使用上述页面进行配置管理。</p>
|
||||
<p>{t('configPage.footer') || '请使用上述页面进行配置管理。'}</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Dropdown, Divider, Spin } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
@@ -12,6 +13,7 @@ import { formatUSDC } from '../utils'
|
||||
const { Option } = Select
|
||||
|
||||
const CopyTradingList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
@@ -73,10 +75,10 @@ const CopyTradingList: React.FC = () => {
|
||||
fetchStatistics(ct.id)
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || '获取跟单列表失败')
|
||||
message.error(response.data.msg || t('copyTradingList.fetchFailed') || '获取跟单列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取跟单列表失败')
|
||||
message.error(error.message || t('copyTradingList.fetchFailed') || '获取跟单列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -133,13 +135,13 @@ const CopyTradingList: React.FC = () => {
|
||||
enabled: !copyTrading.enabled
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success(`${copyTrading.enabled ? '停止' : '开启'}跟单成功`)
|
||||
message.success(copyTrading.enabled ? (t('copyTradingList.stopSuccess') || '停止跟单成功') : (t('copyTradingList.startSuccess') || '开启跟单成功'))
|
||||
fetchCopyTradings()
|
||||
} else {
|
||||
message.error(response.data.msg || '更新跟单状态失败')
|
||||
message.error(response.data.msg || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '更新跟单状态失败')
|
||||
message.error(error.message || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,25 +149,25 @@ const CopyTradingList: React.FC = () => {
|
||||
try {
|
||||
const response = await apiService.copyTrading.delete({ copyTradingId })
|
||||
if (response.data.code === 0) {
|
||||
message.success('删除跟单成功')
|
||||
message.success(t('copyTradingList.deleteSuccess') || '删除跟单成功')
|
||||
fetchCopyTradings()
|
||||
} else {
|
||||
message.error(response.data.msg || '删除跟单失败')
|
||||
message.error(response.data.msg || t('copyTradingList.deleteFailed') || '删除跟单失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除跟单失败')
|
||||
message.error(error.message || t('copyTradingList.deleteFailed') || '删除跟单失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '钱包',
|
||||
title: t('copyTradingList.wallet') || '钱包',
|
||||
key: 'account',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<div>
|
||||
<div style={{ fontSize: isMobile ? 13 : 14, fontWeight: 500 }}>
|
||||
{record.accountName || `账户 ${record.accountId}`}
|
||||
{record.accountName || `${t('copyTradingList.account') || '账户'} ${record.accountId}`}
|
||||
</div>
|
||||
<div style={{ fontSize: isMobile ? 11 : 12, color: '#999', marginTop: 2 }}>
|
||||
{isMobile
|
||||
@@ -177,7 +179,7 @@ const CopyTradingList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '模板',
|
||||
title: t('copyTradingList.template') || '模板',
|
||||
dataIndex: 'templateName',
|
||||
key: 'templateName',
|
||||
width: isMobile ? 100 : 120,
|
||||
@@ -186,7 +188,7 @@ const CopyTradingList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Leader',
|
||||
title: t('copyTradingList.leader') || 'Leader',
|
||||
key: 'leader',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
@@ -204,7 +206,7 @@ const CopyTradingList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('common.status') || '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: isMobile ? 80 : 100,
|
||||
@@ -212,20 +214,20 @@ const CopyTradingList: React.FC = () => {
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => handleToggleStatus(record)}
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="停止"
|
||||
checkedChildren={t('copyTradingList.enabled') || '开启'}
|
||||
unCheckedChildren={t('copyTradingList.disabled') || '停止'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '总盈亏',
|
||||
title: t('copyTradingList.totalPnl') || '总盈亏',
|
||||
key: 'totalPnl',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => {
|
||||
const stats = statisticsMap[record.id]
|
||||
if (!stats) {
|
||||
return loadingStatistics.has(record.id) ? (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>加载中...</span>
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>{t('common.loading') || '加载中...'}</span>
|
||||
) : (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>-</span>
|
||||
)
|
||||
@@ -257,7 +259,7 @@ const CopyTradingList: React.FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('common.actions') || '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 100 : 200,
|
||||
fixed: 'right' as const,
|
||||
@@ -265,25 +267,25 @@ const CopyTradingList: React.FC = () => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'statistics',
|
||||
label: '查看统计',
|
||||
label: t('copyTradingList.viewStatistics') || '查看统计',
|
||||
icon: <BarChartOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'buyOrders',
|
||||
label: '买入订单',
|
||||
label: t('copyTradingList.buyOrders') || '买入订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'sellOrders',
|
||||
label: '卖出订单',
|
||||
label: t('copyTradingList.sellOrders') || '卖出订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: '匹配关系',
|
||||
label: t('copyTradingList.matchedOrders') || '匹配关系',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
},
|
||||
@@ -294,13 +296,13 @@ const CopyTradingList: React.FC = () => {
|
||||
key: 'delete',
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<span style={{ color: '#ff4d4f' }}>删除</span>
|
||||
<span style={{ color: '#ff4d4f' }}>{t('common.delete') || '删除'}</span>
|
||||
</Popconfirm>
|
||||
),
|
||||
danger: true
|
||||
@@ -316,7 +318,7 @@ const CopyTradingList: React.FC = () => {
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
>
|
||||
统计
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
</Button>
|
||||
)}
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||
@@ -325,15 +327,15 @@ const CopyTradingList: React.FC = () => {
|
||||
size="small"
|
||||
icon={<UnorderedListOutlined />}
|
||||
>
|
||||
{isMobile ? '' : '订单'}
|
||||
{isMobile ? '' : (t('copyTradingList.orders') || '订单')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{!isMobile && (
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -341,7 +343,7 @@ const CopyTradingList: React.FC = () => {
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
@@ -355,19 +357,19 @@ const CopyTradingList: React.FC = () => {
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>跟单配置管理</h2>
|
||||
<h2 style={{ margin: 0 }}>{t('copyTradingList.title') || '跟单配置管理'}</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/copy-trading/add')}
|
||||
>
|
||||
新增跟单
|
||||
{t('copyTradingList.addCopyTrading') || '新增跟单'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
placeholder="筛选钱包"
|
||||
placeholder={t('copyTradingList.filterWallet') || '筛选钱包'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.accountId}
|
||||
@@ -375,13 +377,13 @@ const CopyTradingList: React.FC = () => {
|
||||
>
|
||||
{accounts.map(account => (
|
||||
<Option key={account.id} value={account.id}>
|
||||
{account.accountName || `账户 ${account.id}`}
|
||||
{account.accountName || `${t('copyTradingList.account') || '账户'} ${account.id}`}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="筛选模板"
|
||||
placeholder={t('copyTradingList.filterTemplate') || '筛选模板'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.templateId}
|
||||
@@ -395,7 +397,7 @@ const CopyTradingList: React.FC = () => {
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="筛选 Leader"
|
||||
placeholder={t('copyTradingList.filterLeader') || '筛选 Leader'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.leaderId}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, Select, Space, Typography, message } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const LanguageSettings: React.FC = () => {
|
||||
const { t, i18n: i18nInstance } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
// 检测系统语言
|
||||
const detectSystemLanguage = (): string => {
|
||||
const systemLanguage = navigator.language || navigator.languages?.[0] || 'en'
|
||||
const lang = systemLanguage.toLowerCase()
|
||||
|
||||
if (lang.startsWith('zh')) {
|
||||
if (lang.includes('tw') || lang.includes('hk') || lang.includes('mo')) {
|
||||
return 'zh-TW'
|
||||
}
|
||||
return 'zh-CN'
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
// 初始化当前语言设置
|
||||
const getInitialLanguage = (): string => {
|
||||
const savedLanguage = localStorage.getItem('i18n_language')
|
||||
return savedLanguage || 'auto'
|
||||
}
|
||||
|
||||
const [currentLang, setCurrentLang] = useState<string>(getInitialLanguage())
|
||||
|
||||
const languages = [
|
||||
{ value: 'auto', label: t('languageSettings.followSystem') || '跟随系统' },
|
||||
{ value: 'zh-CN', label: '简体中文' },
|
||||
{ value: 'zh-TW', label: '繁體中文' },
|
||||
{ value: 'en', label: 'English' }
|
||||
]
|
||||
|
||||
// 获取当前显示的语言(如果是 auto,显示系统语言)
|
||||
const getDisplayLanguage = (): string => {
|
||||
if (currentLang === 'auto') {
|
||||
return detectSystemLanguage()
|
||||
}
|
||||
return currentLang
|
||||
}
|
||||
|
||||
const handleChange = async (value: string) => {
|
||||
try {
|
||||
let actualLang = value
|
||||
if (value === 'auto') {
|
||||
actualLang = detectSystemLanguage()
|
||||
// 保存 auto 到 localStorage,但使用系统语言
|
||||
localStorage.setItem('i18n_language', 'auto')
|
||||
} else {
|
||||
localStorage.setItem('i18n_language', value)
|
||||
}
|
||||
|
||||
setCurrentLang(value)
|
||||
await i18nInstance.changeLanguage(actualLang)
|
||||
message.success(t('languageSettings.changeSuccess') || '语言切换成功')
|
||||
// 不需要刷新页面,i18n 和 Ant Design 的 locale 会自动更新
|
||||
} catch (error) {
|
||||
message.error(t('languageSettings.changeFailed') || '语言切换失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时,如果当前设置是 auto,确保使用系统语言
|
||||
useEffect(() => {
|
||||
const savedLanguage = localStorage.getItem('i18n_language')
|
||||
if (!savedLanguage || savedLanguage === 'auto') {
|
||||
const systemLang = detectSystemLanguage()
|
||||
if (i18nInstance.language !== systemLang) {
|
||||
i18nInstance.changeLanguage(systemLang)
|
||||
}
|
||||
} else {
|
||||
// 如果保存的是具体语言,确保使用该语言
|
||||
if (i18nInstance.language !== savedLanguage) {
|
||||
i18nInstance.changeLanguage(savedLanguage)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('languageSettings.title') || '语言设置'}</Title>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
|
||||
{t('languageSettings.currentLanguage') || '当前语言'}
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={currentLang}
|
||||
onChange={handleChange}
|
||||
options={languages}
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
/>
|
||||
{currentLang === 'auto' && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('languageSettings.currentSystemLanguage') || '当前系统语言'}: {
|
||||
getDisplayLanguage() === 'zh-CN' ? '简体中文' :
|
||||
getDisplayLanguage() === 'zh-TW' ? '繁體中文' : 'English'
|
||||
}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">
|
||||
{t('languageSettings.description') || '切换语言后,界面将立即更新为新语言。'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageSettings
|
||||
|
||||
@@ -2,11 +2,13 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { Leader } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const LeaderList: React.FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [leaders, setLeaders] = useState<Leader[]>([])
|
||||
@@ -23,10 +25,10 @@ const LeaderList: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setLeaders(response.data.data.list || [])
|
||||
} else {
|
||||
message.error(response.data.msg || '获取 Leader 列表失败')
|
||||
message.error(response.data.msg || t('leaderList.fetchFailed') || '获取 Leader 列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取 Leader 列表失败')
|
||||
message.error(error.message || t('leaderList.fetchFailed') || '获取 Leader 列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -36,25 +38,25 @@ const LeaderList: React.FC = () => {
|
||||
try {
|
||||
const response = await apiService.leaders.delete({ leaderId })
|
||||
if (response.data.code === 0) {
|
||||
message.success('删除 Leader 成功')
|
||||
message.success(t('leaderList.deleteSuccess') || '删除 Leader 成功')
|
||||
fetchLeaders()
|
||||
} else {
|
||||
message.error(response.data.msg || '删除 Leader 失败')
|
||||
message.error(response.data.msg || t('leaderList.deleteFailed') || '删除 Leader 失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除 Leader 失败')
|
||||
message.error(error.message || t('leaderList.deleteFailed') || '删除 Leader 失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Leader 名称',
|
||||
title: t('leaderList.leaderName') || 'Leader 名称',
|
||||
dataIndex: 'leaderName',
|
||||
key: 'leaderName',
|
||||
render: (text: string, record: Leader) => text || `Leader ${record.id}`
|
||||
},
|
||||
{
|
||||
title: '钱包地址',
|
||||
title: t('leaderList.walletAddress') || '钱包地址',
|
||||
dataIndex: 'leaderAddress',
|
||||
key: 'leaderAddress',
|
||||
render: (address: string) => (
|
||||
@@ -64,26 +66,26 @@ const LeaderList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
title: t('leaderList.category') || '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (category: string | undefined) => category ? (
|
||||
<Tag color={category === 'sports' ? 'blue' : 'green'}>{category}</Tag>
|
||||
) : <Tag>全部</Tag>
|
||||
) : <Tag>{t('leaderList.all') || '全部'}</Tag>
|
||||
},
|
||||
{
|
||||
title: '跟单关系数',
|
||||
title: t('leaderList.copyTradingCount') || '跟单关系数',
|
||||
dataIndex: 'copyTradingCount',
|
||||
key: 'copyTradingCount',
|
||||
render: (count: number) => <Tag>{count}</Tag>
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('leaderList.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
return date.toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -93,7 +95,7 @@ const LeaderList: React.FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('common.actions') || '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 150,
|
||||
render: (_: any, record: Leader) => (
|
||||
@@ -104,17 +106,17 @@ const LeaderList: React.FC = () => {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/leaders/edit?id=${record.id}`)}
|
||||
>
|
||||
编辑
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个 Leader 吗?"
|
||||
description={record.copyTradingCount > 0 ? `该 Leader 还有 ${record.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
|
||||
title={t('leaderList.deleteConfirm') || '确定要删除这个 Leader 吗?'}
|
||||
description={record.copyTradingCount > 0 ? t('leaderList.deleteConfirmDesc', { count: record.copyTradingCount }) || `该 Leader 还有 ${record.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -132,14 +134,14 @@ const LeaderList: React.FC = () => {
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<h2 style={{ margin: 0 }}>Leader 管理</h2>
|
||||
<h2 style={{ margin: 0 }}>{t('leaderList.title') || 'Leader 管理'}</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/leaders/add')}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
添加 Leader
|
||||
{t('leaderList.addLeader') || '添加 Leader'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -152,13 +154,13 @@ const LeaderList: React.FC = () => {
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : leaders.length === 0 ? (
|
||||
<Empty description="暂无 Leader 数据" />
|
||||
<Empty description={t('leaderList.noData') || '暂无 Leader 数据'} />
|
||||
) : (
|
||||
<List
|
||||
dataSource={leaders}
|
||||
renderItem={(leader) => {
|
||||
const date = new Date(leader.createdAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -207,15 +209,15 @@ const LeaderList: React.FC = () => {
|
||||
{leader.category}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag>全部</Tag>
|
||||
<Tag>{t('leaderList.all') || '全部'}</Tag>
|
||||
)}
|
||||
<Tag>{leader.copyTradingCount} 个跟单关系</Tag>
|
||||
<Tag>{t('leaderList.copyTradingRelations', { count: leader.copyTradingCount }) || `${leader.copyTradingCount} 个跟单关系`}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '12px', fontSize: '12px', color: '#999' }}>
|
||||
创建时间: {formattedDate}
|
||||
{t('leaderList.createdAt') || '创建时间'}: {formattedDate}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
@@ -227,14 +229,14 @@ const LeaderList: React.FC = () => {
|
||||
onClick={() => navigate(`/leaders/edit?id=${leader.id}`)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
编辑
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个 Leader 吗?"
|
||||
description={leader.copyTradingCount > 0 ? `该 Leader 还有 ${leader.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
|
||||
title={t('leaderList.deleteConfirm') || '确定要删除这个 Leader 吗?'}
|
||||
description={leader.copyTradingCount > 0 ? t('leaderList.deleteConfirmDesc', { count: leader.copyTradingCount }) || `该 Leader 还有 ${leader.copyTradingCount} 个跟单关系,请先删除跟单关系` : undefined}
|
||||
onConfirm={() => handleDelete(leader.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -243,7 +245,7 @@ const LeaderList: React.FC = () => {
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography } from 'antd'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { setToken } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -9,6 +10,7 @@ import { useMediaQuery } from 'react-responsive'
|
||||
const { Title } = Typography
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -21,15 +23,15 @@ const Login: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const token = response.data.data.token
|
||||
setToken(token)
|
||||
message.success('登录成功')
|
||||
message.success(t('message.loginSuccess'))
|
||||
// 跳转到首页
|
||||
navigate('/')
|
||||
} else {
|
||||
message.error(response.data.msg || '登录失败')
|
||||
message.error(response.data.msg || t('message.loginFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '登录失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('message.loginFailed')
|
||||
message.error(errorMsg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -52,7 +54,7 @@ const Login: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
登录
|
||||
{t('login.title')}
|
||||
</Title>
|
||||
<Form
|
||||
form={form}
|
||||
@@ -62,25 +64,27 @@ const Login: React.FC = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={t('login.username')}
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' }
|
||||
{ required: true, message: t('login.usernameRequired') }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t('login.password')}
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' }
|
||||
{ required: true, message: t('login.passwordRequired') }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -92,12 +96,12 @@ const Login: React.FC = () => {
|
||||
loading={loading}
|
||||
size={isMobile ? 'large' : 'middle'}
|
||||
>
|
||||
登录
|
||||
{t('login.title')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Link to="/reset-password" style={{ fontSize: isMobile ? '14px' : '13px' }}>
|
||||
忘记密码?重置密码
|
||||
{t('login.forgotPassword')}
|
||||
</Link>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -107,4 +111,3 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Tag, message } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { CopyOrder } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
|
||||
const OrderList: React.FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [orders, setOrders] = useState<CopyOrder[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -33,10 +35,10 @@ const OrderList: React.FC = () => {
|
||||
total: response.data.data?.total || 0
|
||||
}))
|
||||
} else {
|
||||
message.error(response.data.msg || '获取订单列表失败')
|
||||
message.error(response.data.msg || t('orderList.fetchFailed') || '获取订单列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取订单列表失败')
|
||||
message.error(error.message || t('orderList.fetchFailed') || '获取订单列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -61,13 +63,13 @@ const OrderList: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Leader',
|
||||
title: t('orderList.leader') || 'Leader',
|
||||
dataIndex: 'leaderName',
|
||||
key: 'leaderName',
|
||||
render: (text: string, record: CopyOrder) => text || record.leaderAddress.slice(0, 10) + '...'
|
||||
},
|
||||
{
|
||||
title: '市场',
|
||||
title: t('orderList.market') || '市场',
|
||||
dataIndex: 'marketId',
|
||||
key: 'marketId',
|
||||
render: (marketId: string) => (
|
||||
@@ -77,7 +79,7 @@ const OrderList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
title: t('orderList.category') || '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (category: string) => (
|
||||
@@ -85,7 +87,7 @@ const OrderList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
title: t('orderList.side') || '方向',
|
||||
dataIndex: 'side',
|
||||
key: 'side',
|
||||
render: (side: string) => (
|
||||
@@ -93,17 +95,17 @@ const OrderList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
title: t('orderList.price') || '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price'
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
title: t('orderList.size') || '数量',
|
||||
dataIndex: 'size',
|
||||
key: 'size'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('orderList.status') || '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
@@ -111,7 +113,7 @@ const OrderList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '盈亏',
|
||||
title: t('orderList.pnl') || '盈亏',
|
||||
dataIndex: 'pnl',
|
||||
key: 'pnl',
|
||||
render: (pnl: string | undefined) => pnl ? (
|
||||
@@ -121,17 +123,17 @@ const OrderList: React.FC = () => {
|
||||
) : '-'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('orderList.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString()
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString(i18n.language || 'zh-CN')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<h2>订单管理</h2>
|
||||
<h2>{t('orderList.title') || '订单管理'}</h2>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert } from 'antd'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface ProxyConfig {
|
||||
id?: number
|
||||
type: string
|
||||
enabled: boolean
|
||||
host?: string
|
||||
port?: number
|
||||
username?: string
|
||||
subscriptionUrl?: string
|
||||
lastSubscriptionUpdate?: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
interface ProxyCheckResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number
|
||||
}
|
||||
|
||||
const ProxySettings: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [checkResult, setCheckResult] = useState<ProxyCheckResponse | null>(null)
|
||||
const [currentConfig, setCurrentConfig] = useState<ProxyConfig | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig()
|
||||
}, [])
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await apiService.proxyConfig.get()
|
||||
if (response.data.code === 0) {
|
||||
const data = response.data.data
|
||||
setCurrentConfig(data)
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
enabled: data.enabled,
|
||||
host: data.host || '',
|
||||
port: data.port || undefined,
|
||||
username: data.username || '',
|
||||
password: '', // 密码不预填充
|
||||
})
|
||||
} else {
|
||||
form.resetFields()
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || t('proxySettings.getFailed') || '获取代理配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('proxySettings.getFailed') || '获取代理配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const requestData: any = {
|
||||
enabled: values.enabled || false,
|
||||
host: values.host,
|
||||
port: values.port,
|
||||
username: values.username || undefined,
|
||||
}
|
||||
|
||||
// 只有在输入了新密码时才包含密码字段
|
||||
if (values.password && values.password.trim()) {
|
||||
requestData.password = values.password
|
||||
}
|
||||
|
||||
const response = await apiService.proxyConfig.saveHttp(requestData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('proxySettings.saveSuccess') || '保存配置成功')
|
||||
setCheckResult(null)
|
||||
fetchConfig()
|
||||
} else {
|
||||
message.error(response.data.msg || t('proxySettings.saveFailed') || '保存配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('proxySettings.saveFailed') || '保存配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = async () => {
|
||||
setChecking(true)
|
||||
setCheckResult(null)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.check()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setCheckResult(response.data.data)
|
||||
} else {
|
||||
setCheckResult({
|
||||
success: false,
|
||||
message: response.data.msg || t('proxySettings.checkFailed') || '代理检查失败'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
setCheckResult({
|
||||
success: false,
|
||||
message: error.message || t('proxySettings.checkFailed') || '代理检查失败'
|
||||
})
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('proxySettings.title') || '代理设置'}</Title>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('proxySettings.enabled') || '启用代理'}
|
||||
name="enabled"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('proxySettings.host') || '代理主机'}
|
||||
name="host"
|
||||
rules={[
|
||||
{ required: true, message: t('proxySettings.hostRequired') || '请输入代理主机地址' },
|
||||
{ pattern: /^[\w\.-]+$/, message: t('proxySettings.hostInvalid') || '请输入有效的主机地址' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('proxySettings.hostPlaceholder') || '例如:127.0.0.1 或 proxy.example.com'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('proxySettings.port') || '代理端口'}
|
||||
name="port"
|
||||
rules={[
|
||||
{ required: true, message: t('proxySettings.portRequired') || '请输入代理端口' },
|
||||
{ type: 'number', min: 1, max: 65535, message: t('proxySettings.portInvalid') || '端口必须在 1-65535 之间' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('proxySettings.portPlaceholder') || '例如:8888'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('proxySettings.username') || '代理用户名(可选)'}
|
||||
name="username"
|
||||
>
|
||||
<Input placeholder={t('proxySettings.usernamePlaceholder') || '如果代理需要认证,请输入用户名'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('proxySettings.password') || '代理密码(可选)'}
|
||||
name="password"
|
||||
help={currentConfig ? (t('proxySettings.passwordHelpUpdate') || '留空则不更新密码,输入新密码则更新') : (t('proxySettings.passwordHelp') || '如果代理需要认证,请输入密码')}
|
||||
>
|
||||
<Input.Password placeholder={currentConfig ? (t('proxySettings.passwordPlaceholderUpdate') || '留空则不更新密码') : (t('proxySettings.passwordPlaceholder') || '如果代理需要认证,请输入密码')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={loading}
|
||||
>
|
||||
{t('common.save') || '保存配置'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={handleCheck}
|
||||
loading={checking}
|
||||
>
|
||||
{t('proxySettings.check') || '检查代理'}
|
||||
</Button>
|
||||
{checkResult && (
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchConfig}
|
||||
>
|
||||
{t('common.refresh') || '刷新配置'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{checkResult && (
|
||||
<Alert
|
||||
type={checkResult.success ? 'success' : 'error'}
|
||||
message={checkResult.success ? (t('proxySettings.checkSuccess') || '代理检查成功') : (t('proxySettings.checkFailed') || '代理检查失败')}
|
||||
description={
|
||||
<div>
|
||||
<Text>{checkResult.message}</Text>
|
||||
{(checkResult.responseTime !== undefined || checkResult.latency !== undefined) && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">
|
||||
{t('proxySettings.latency') || '延迟'}: {(checkResult.latency ?? checkResult.responseTime) ?? 0}ms
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{ marginTop: '16px' }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProxySettings
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, Form, Input, Button, message, Typography, Alert, Progress } from 'antd'
|
||||
import { LockOutlined, KeyOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
@@ -30,32 +31,33 @@ const getPasswordStrength = (password: string): number => {
|
||||
return Math.min(4, Math.floor(strength))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取密码强度文本和颜色
|
||||
*/
|
||||
const getPasswordStrengthInfo = (strength: number): { text: string; color: string; percent: number } => {
|
||||
switch (strength) {
|
||||
case 0:
|
||||
return { text: '弱', color: '#ff4d4f', percent: 25 }
|
||||
case 1:
|
||||
return { text: '较弱', color: '#ff7a45', percent: 50 }
|
||||
case 2:
|
||||
return { text: '中等', color: '#faad14', percent: 75 }
|
||||
case 3:
|
||||
return { text: '强', color: '#52c41a', percent: 100 }
|
||||
case 4:
|
||||
return { text: '很强', color: '#52c41a', percent: 100 }
|
||||
default:
|
||||
return { text: '弱', color: '#ff4d4f', percent: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [passwordStrength, setPasswordStrength] = useState(0)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
/**
|
||||
* 获取密码强度文本和颜色
|
||||
*/
|
||||
const getPasswordStrengthInfo = (strength: number): { text: string; color: string; percent: number } => {
|
||||
switch (strength) {
|
||||
case 0:
|
||||
return { text: t('resetPassword.weak') || '弱', color: '#ff4d4f', percent: 25 }
|
||||
case 1:
|
||||
return { text: t('resetPassword.fair') || '较弱', color: '#ff7a45', percent: 50 }
|
||||
case 2:
|
||||
return { text: t('resetPassword.medium') || '中等', color: '#faad14', percent: 75 }
|
||||
case 3:
|
||||
return { text: t('resetPassword.strong') || '强', color: '#52c41a', percent: 100 }
|
||||
case 4:
|
||||
return { text: t('resetPassword.veryStrong') || '很强', color: '#52c41a', percent: 100 }
|
||||
default:
|
||||
return { text: t('resetPassword.weak') || '弱', color: '#ff4d4f', percent: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async (values: {
|
||||
resetKey: string
|
||||
username: string
|
||||
@@ -63,7 +65,7 @@ const ResetPassword: React.FC = () => {
|
||||
confirmPassword: string
|
||||
}) => {
|
||||
if (values.newPassword !== values.confirmPassword) {
|
||||
message.error('两次输入的密码不一致')
|
||||
message.error(t('resetPassword.passwordMismatch') || '两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,17 +77,17 @@ const ResetPassword: React.FC = () => {
|
||||
newPassword: values.newPassword
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('密码重置成功', 1)
|
||||
message.success(t('resetPassword.success') || '密码重置成功', 1)
|
||||
// 使用 window.location.href 强制跳转到登录页,确保跳转成功
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 500)
|
||||
} else {
|
||||
message.error(response.data.msg || '密码重置失败')
|
||||
message.error(response.data.msg || t('resetPassword.failed') || '密码重置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('密码重置失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '密码重置失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('resetPassword.failed') || '密码重置失败'
|
||||
message.error(errorMsg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -108,11 +110,11 @@ const ResetPassword: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
重置密码
|
||||
{t('resetPassword.title') || '重置密码'}
|
||||
</Title>
|
||||
<Alert
|
||||
message="首次使用系统"
|
||||
description="请使用管理员提供的重置密钥设置初始密码"
|
||||
message={t('resetPassword.firstUse') || '首次使用系统'}
|
||||
description={t('resetPassword.firstUseDesc') || '请使用管理员提供的重置密钥设置初始密码'}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
@@ -125,39 +127,39 @@ const ResetPassword: React.FC = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="resetKey"
|
||||
label="重置密钥"
|
||||
label={t('resetPassword.resetKey') || '重置密钥'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入重置密钥' }
|
||||
{ required: true, message: t('resetPassword.resetKeyRequired') || '请输入重置密钥' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<KeyOutlined />}
|
||||
placeholder="请输入重置密钥"
|
||||
placeholder={t('resetPassword.resetKeyPlaceholder') || '请输入重置密钥'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
label={t('resetPassword.username') || '用户名'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' }
|
||||
{ required: true, message: t('resetPassword.usernameRequired') || '请输入用户名' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="请输入用户名"
|
||||
placeholder={t('resetPassword.usernamePlaceholder') || '请输入用户名'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
label={t('resetPassword.newPassword') || '新密码'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码至少6位' }
|
||||
{ required: true, message: t('resetPassword.newPasswordRequired') || '请输入新密码' },
|
||||
{ min: 6, message: t('resetPassword.passwordMinLength') || '密码至少6位' }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="至少6位"
|
||||
placeholder={t('resetPassword.passwordPlaceholder') || '至少6位'}
|
||||
onChange={(e) => {
|
||||
const strength = getPasswordStrength(e.target.value)
|
||||
setPasswordStrength(strength)
|
||||
@@ -168,7 +170,7 @@ const ResetPassword: React.FC = () => {
|
||||
<Form.Item>
|
||||
<div style={{ marginTop: '-16px', marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>密码强度:</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>{t('resetPassword.passwordStrength') || '密码强度'}:</span>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
@@ -188,23 +190,23 @@ const ResetPassword: React.FC = () => {
|
||||
)}
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认密码"
|
||||
label={t('resetPassword.confirmPassword') || '确认密码'}
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
{ required: true, message: t('resetPassword.confirmPasswordRequired') || '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'))
|
||||
return Promise.reject(new Error(t('resetPassword.passwordMismatch') || '两次输入的密码不一致'))
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请再次输入密码"
|
||||
placeholder={t('resetPassword.confirmPasswordPlaceholder') || '请再次输入密码'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
@@ -215,7 +217,7 @@ const ResetPassword: React.FC = () => {
|
||||
loading={loading}
|
||||
size={isMobile ? 'large' : 'middle'}
|
||||
>
|
||||
重置密码
|
||||
{t('resetPassword.submit') || '重置密码'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Row, Col, Statistic, message, DatePicker, Space, Button, Typography } from 'antd'
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { apiService } from '../services/api'
|
||||
import type { Statistics as StatisticsType } from '../types'
|
||||
@@ -11,6 +12,7 @@ const { RangePicker } = DatePicker
|
||||
const { Title } = Typography
|
||||
|
||||
const Statistics: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [stats, setStats] = useState<StatisticsType | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -30,10 +32,10 @@ const Statistics: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStats(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取统计信息失败')
|
||||
message.error(response.data.msg || t('statistics.fetchFailed') || '获取统计信息失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取统计信息失败')
|
||||
message.error(error.message || t('statistics.fetchFailed') || '获取统计信息失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -54,13 +56,13 @@ const Statistics: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>统计信息</Title>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('statistics.title') || '统计信息'}</Title>
|
||||
<Space size="middle" wrap>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
onChange={handleDateRangeChange}
|
||||
format="YYYY-MM-DD"
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
placeholder={[t('statistics.startDate') || '开始日期', t('statistics.endDate') || '结束日期']}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
allowClear
|
||||
/>
|
||||
@@ -71,14 +73,14 @@ const Statistics: React.FC = () => {
|
||||
loading={loading}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
刷新
|
||||
{t('statistics.refresh') || '刷新'}
|
||||
</Button>
|
||||
{(dateRange[0] || dateRange[1]) && (
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
重置
|
||||
{t('statistics.reset') || '重置'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
@@ -88,7 +90,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总订单数"
|
||||
title={t('statistics.totalOrders') || '总订单数'}
|
||||
value={stats?.totalOrders || 0}
|
||||
loading={loading}
|
||||
/>
|
||||
@@ -97,7 +99,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总盈亏"
|
||||
title={t('statistics.totalPnl') || '总盈亏'}
|
||||
value={formatUSDC(stats?.totalPnl || '0')}
|
||||
prefix={stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||
valueStyle={{ color: stats?.totalPnl && parseFloat(stats.totalPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
|
||||
@@ -109,7 +111,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="胜率"
|
||||
title={t('statistics.winRate') || '胜率'}
|
||||
value={stats?.winRate || '0'}
|
||||
precision={2}
|
||||
suffix="%"
|
||||
@@ -120,7 +122,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="平均盈亏"
|
||||
title={t('statistics.avgPnl') || '平均盈亏'}
|
||||
value={formatUSDC(stats?.avgPnl || '0')}
|
||||
prefix={stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||
valueStyle={{ color: stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
|
||||
@@ -132,7 +134,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最大盈利"
|
||||
title={t('statistics.maxProfit') || '最大盈利'}
|
||||
value={formatUSDC(stats?.maxProfit || '0')}
|
||||
prefix={<ArrowUpOutlined />}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
@@ -144,7 +146,7 @@ const Statistics: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最大亏损"
|
||||
title={t('statistics.maxLoss') || '最大亏损'}
|
||||
value={formatUSDC(stats?.maxLoss || '0')}
|
||||
prefix={<ArrowDownOutlined />}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Input, Modal, Form, Radio, InputNumber, Switch, Divider, Spin } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { CopyTradingTemplate } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -10,6 +11,7 @@ import { formatUSDC } from '../utils'
|
||||
const { Search } = Input
|
||||
|
||||
const TemplateList: React.FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [templates, setTemplates] = useState<CopyTradingTemplate[]>([])
|
||||
@@ -32,10 +34,10 @@ const TemplateList: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setTemplates(response.data.data.list || [])
|
||||
} else {
|
||||
message.error(response.data.msg || '获取模板列表失败')
|
||||
message.error(response.data.msg || t('templateList.fetchFailed') || '获取模板列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取模板列表失败')
|
||||
message.error(error.message || t('templateList.fetchFailed') || '获取模板列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -45,13 +47,13 @@ const TemplateList: React.FC = () => {
|
||||
try {
|
||||
const response = await apiService.templates.delete({ templateId })
|
||||
if (response.data.code === 0) {
|
||||
message.success('删除模板成功')
|
||||
message.success(t('templateList.deleteSuccess') || '删除模板成功')
|
||||
fetchTemplates()
|
||||
} else {
|
||||
message.error(response.data.msg || '删除模板失败')
|
||||
message.error(response.data.msg || t('templateList.deleteFailed') || '删除模板失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除模板失败')
|
||||
message.error(error.message || t('templateList.deleteFailed') || '删除模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +63,7 @@ const TemplateList: React.FC = () => {
|
||||
|
||||
// 填充表单数据
|
||||
copyForm.setFieldsValue({
|
||||
templateName: `${template.templateName}-副本`,
|
||||
templateName: `${template.templateName}-${t('templateList.copySuffix') || '副本'}`,
|
||||
copyMode: template.copyMode,
|
||||
copyRatio: template.copyRatio ? parseFloat(template.copyRatio) * 100 : 100,
|
||||
fixedAmount: template.fixedAmount ? parseFloat(template.fixedAmount) : undefined,
|
||||
@@ -78,7 +80,7 @@ const TemplateList: React.FC = () => {
|
||||
const handleCopySubmit = async (values: any) => {
|
||||
// 前端校验:如果填写了 minOrderSize,必须 >= 1
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && values.minOrderSize !== '' && Number(values.minOrderSize) < 1) {
|
||||
message.error('最小金额必须 >= 1')
|
||||
message.error(t('templateList.minAmountError') || '最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,16 +88,16 @@ const TemplateList: React.FC = () => {
|
||||
if (values.copyMode === 'FIXED') {
|
||||
const fixedAmount = values.fixedAmount
|
||||
if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') {
|
||||
message.error('请输入固定跟单金额')
|
||||
message.error(t('templateList.fixedAmountRequired') || '请输入固定跟单金额')
|
||||
return
|
||||
}
|
||||
const amount = Number(fixedAmount)
|
||||
if (isNaN(amount)) {
|
||||
message.error('请输入有效的数字')
|
||||
message.error(t('templateList.invalidNumber') || '请输入有效的数字')
|
||||
return
|
||||
}
|
||||
if (amount < 1) {
|
||||
message.error('固定金额必须 >= 1,请重新输入')
|
||||
message.error(t('templateList.fixedAmountError') || '固定金额必须 >= 1,请重新输入')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -116,15 +118,15 @@ const TemplateList: React.FC = () => {
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success('复制模板成功')
|
||||
message.success(t('templateList.copySuccess') || '复制模板成功')
|
||||
setCopyModalVisible(false)
|
||||
copyForm.resetFields()
|
||||
fetchTemplates()
|
||||
} else {
|
||||
message.error(response.data.msg || '复制模板失败')
|
||||
message.error(response.data.msg || t('templateList.copyFailed') || '复制模板失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '复制模板失败')
|
||||
message.error(error.message || t('templateList.copyFailed') || '复制模板失败')
|
||||
} finally {
|
||||
setCopyLoading(false)
|
||||
}
|
||||
@@ -142,56 +144,56 @@ const TemplateList: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模板名称',
|
||||
title: t('templateList.templateName') || '模板名称',
|
||||
dataIndex: 'templateName',
|
||||
key: 'templateName',
|
||||
render: (text: string) => <strong>{text}</strong>
|
||||
},
|
||||
{
|
||||
title: '跟单模式',
|
||||
title: t('templateList.copyMode') || '跟单模式',
|
||||
dataIndex: 'copyMode',
|
||||
key: 'copyMode',
|
||||
render: (mode: string) => (
|
||||
<Tag color={mode === 'RATIO' ? 'blue' : 'green'}>
|
||||
{mode === 'RATIO' ? '比例' : '固定金额'}
|
||||
{mode === 'RATIO' ? t('templateList.ratio') || '比例' : t('templateList.fixedAmount') || '固定金额'}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '跟单配置',
|
||||
title: t('templateList.copyConfig') || '跟单配置',
|
||||
key: 'copyConfig',
|
||||
render: (_: any, record: CopyTradingTemplate) => {
|
||||
if (record.copyMode === 'RATIO') {
|
||||
return `比例 ${record.copyRatio}x`
|
||||
return `${t('templateList.ratio') || '比例'} ${record.copyRatio}x`
|
||||
} else if (record.copyMode === 'FIXED' && record.fixedAmount) {
|
||||
return `固定 ${formatUSDC(record.fixedAmount)} USDC`
|
||||
return `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(record.fixedAmount)} USDC`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '跟单卖出',
|
||||
title: t('templateList.supportSell') || '跟单卖出',
|
||||
dataIndex: 'supportSell',
|
||||
key: 'supportSell',
|
||||
render: (support: boolean) => (
|
||||
<Tag color={support ? 'green' : 'red'}>
|
||||
{support ? '是' : '否'}
|
||||
{support ? t('common.yes') || '是' : t('common.no') || '否'}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '使用次数',
|
||||
title: t('templateList.useCount') || '使用次数',
|
||||
dataIndex: 'useCount',
|
||||
key: 'useCount',
|
||||
render: (count: number) => <Tag>{count}</Tag>
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('common.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
return date.toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -204,7 +206,7 @@ const TemplateList: React.FC = () => {
|
||||
defaultSortOrder: 'descend' as const
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('common.actions') || '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 200,
|
||||
render: (_: any, record: CopyTradingTemplate) => (
|
||||
@@ -215,7 +217,7 @@ const TemplateList: React.FC = () => {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/templates/edit/${record.id}`)}
|
||||
>
|
||||
编辑
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -223,14 +225,14 @@ const TemplateList: React.FC = () => {
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record)}
|
||||
>
|
||||
复制
|
||||
{t('templateList.copy') || '复制'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
description="删除后无法恢复,请确保没有跟单关系在使用该模板"
|
||||
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
|
||||
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -238,7 +240,7 @@ const TemplateList: React.FC = () => {
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -250,10 +252,10 @@ const TemplateList: React.FC = () => {
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>跟单模板管理</h2>
|
||||
<h2 style={{ margin: 0 }}>{t('templateList.title') || '跟单模板管理'}</h2>
|
||||
<Space>
|
||||
<Search
|
||||
placeholder="搜索模板名称"
|
||||
placeholder={t('templateList.searchPlaceholder') || '搜索模板名称'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? 150 : 250 }}
|
||||
onSearch={setSearchText}
|
||||
@@ -264,7 +266,7 @@ const TemplateList: React.FC = () => {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/templates/add')}
|
||||
>
|
||||
新增模板
|
||||
{t('templateList.addTemplate') || '新增模板'}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -278,13 +280,13 @@ const TemplateList: React.FC = () => {
|
||||
</div>
|
||||
) : filteredTemplates.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无模板数据
|
||||
{t('templateList.noData') || '暂无模板数据'}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{filteredTemplates.map((template) => {
|
||||
const date = new Date(template.createdAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -314,12 +316,12 @@ const TemplateList: React.FC = () => {
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag color={template.copyMode === 'RATIO' ? 'blue' : 'green'}>
|
||||
{template.copyMode === 'RATIO' ? '比例模式' : '固定金额模式'}
|
||||
{template.copyMode === 'RATIO' ? (t('templateList.ratioMode') || '比例模式') : (t('templateList.fixedAmountMode') || '固定金额模式')}
|
||||
</Tag>
|
||||
<Tag color={template.supportSell ? 'green' : 'red'}>
|
||||
{template.supportSell ? '跟单卖出' : '不跟单卖出'}
|
||||
{template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
|
||||
</Tag>
|
||||
<Tag>{template.useCount} 次使用</Tag>
|
||||
<Tag>{template.useCount} {t('templateList.timesUsed') || '次使用'}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -327,12 +329,12 @@ const TemplateList: React.FC = () => {
|
||||
|
||||
{/* 跟单配置 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>跟单配置</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.copyConfig') || '跟单配置'}</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{template.copyMode === 'RATIO'
|
||||
? `比例 ${template.copyRatio}x`
|
||||
? `${t('templateList.ratio') || '比例'} ${template.copyRatio}x`
|
||||
: template.fixedAmount
|
||||
? `固定 ${formatUSDC(template.fixedAmount)} USDC`
|
||||
? `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(template.fixedAmount)} USDC`
|
||||
: '-'
|
||||
}
|
||||
</div>
|
||||
@@ -341,31 +343,31 @@ const TemplateList: React.FC = () => {
|
||||
{/* 其他配置信息 */}
|
||||
{template.copyMode === 'RATIO' && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>金额限制</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.amountLimit') || '金额限制'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{template.maxOrderSize && (
|
||||
<span>最大: {formatUSDC(template.maxOrderSize)} USDC</span>
|
||||
<span>{t('templateList.max') || '最大'}: {formatUSDC(template.maxOrderSize)} USDC</span>
|
||||
)}
|
||||
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
|
||||
{template.minOrderSize && (
|
||||
<span>最小: {formatUSDC(template.minOrderSize)} USDC</span>
|
||||
<span>{t('templateList.min') || '最小'}: {formatUSDC(template.minOrderSize)} USDC</span>
|
||||
)}
|
||||
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#999' }}>未设置</span>}
|
||||
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#999' }}>{t('templateList.notSet') || '未设置'}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>其他配置</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.otherConfig') || '其他配置'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
每日最大订单: {template.maxDailyOrders} | 价格容忍度: {template.priceTolerance}%
|
||||
{t('templateList.maxDailyOrders') || '每日最大订单'}: {template.maxDailyOrders} | {t('templateList.priceTolerance') || '价格容忍度'}: {template.priceTolerance}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
创建时间: {formattedDate}
|
||||
{t('common.createdAt') || '创建时间'}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -378,7 +380,7 @@ const TemplateList: React.FC = () => {
|
||||
onClick={() => navigate(`/templates/edit/${template.id}`)}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
编辑
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -386,14 +388,14 @@ const TemplateList: React.FC = () => {
|
||||
onClick={() => handleCopy(template)}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
复制
|
||||
{t('templateList.copy') || '复制'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
description="删除后无法恢复,请确保没有跟单关系在使用该模板"
|
||||
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
|
||||
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
|
||||
onConfirm={() => handleDelete(template.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
@@ -401,7 +403,7 @@ const TemplateList: React.FC = () => {
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
@@ -15,6 +16,7 @@ interface User {
|
||||
}
|
||||
|
||||
const UserList: React.FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -37,11 +39,11 @@ const UserList: React.FC = () => {
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setUsers(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取用户列表失败')
|
||||
message.error(response.data.msg || t('userList.fetchFailed') || '获取用户列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '获取用户列表失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('userList.fetchFailed') || '获取用户列表失败'
|
||||
message.error(errorMsg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -59,16 +61,16 @@ const UserList: React.FC = () => {
|
||||
password: values.password
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('创建用户成功')
|
||||
message.success(t('userList.createSuccess') || '创建用户成功')
|
||||
setCreateModalVisible(false)
|
||||
createForm.resetFields()
|
||||
fetchUsers()
|
||||
} else {
|
||||
message.error(response.data.msg || '创建用户失败')
|
||||
message.error(response.data.msg || t('userList.createFailed') || '创建用户失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('创建用户失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '创建用户失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('userList.createFailed') || '创建用户失败'
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -82,17 +84,17 @@ const UserList: React.FC = () => {
|
||||
newPassword: values.newPassword
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('更新密码成功')
|
||||
message.success(t('userList.updatePasswordSuccess') || '更新密码成功')
|
||||
setUpdatePasswordModalVisible(false)
|
||||
setSelectedUser(null)
|
||||
updatePasswordForm.resetFields()
|
||||
fetchUsers()
|
||||
} else {
|
||||
message.error(response.data.msg || '更新密码失败')
|
||||
message.error(response.data.msg || t('userList.updatePasswordFailed') || '更新密码失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('更新密码失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '更新密码失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('userList.updatePasswordFailed') || '更新密码失败'
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +105,7 @@ const UserList: React.FC = () => {
|
||||
newPassword: values.newPassword
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('修改密码成功,请重新登录')
|
||||
message.success(t('userList.updateOwnPasswordSuccess') || '修改密码成功,请重新登录')
|
||||
setUpdateOwnPasswordModalVisible(false)
|
||||
updateOwnPasswordForm.resetFields()
|
||||
// 延迟跳转到登录页
|
||||
@@ -111,11 +113,11 @@ const UserList: React.FC = () => {
|
||||
window.location.href = '/login'
|
||||
}, 1000)
|
||||
} else {
|
||||
message.error(response.data.msg || '修改密码失败')
|
||||
message.error(response.data.msg || t('userList.updateOwnPasswordFailed') || '修改密码失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('修改密码失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '修改密码失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('userList.updateOwnPasswordFailed') || '修改密码失败'
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -124,14 +126,14 @@ const UserList: React.FC = () => {
|
||||
try {
|
||||
const response = await apiService.users.delete({ userId: user.id })
|
||||
if (response.data.code === 0) {
|
||||
message.success('删除用户成功')
|
||||
message.success(t('userList.deleteSuccess') || '删除用户成功')
|
||||
fetchUsers()
|
||||
} else {
|
||||
message.error(response.data.msg || '删除用户失败')
|
||||
message.error(response.data.msg || t('userList.deleteFailed') || '删除用户失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('删除用户失败:', error)
|
||||
const errorMsg = error.response?.data?.msg || error.message || '删除用户失败'
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('userList.deleteFailed') || '删除用户失败'
|
||||
message.error(errorMsg)
|
||||
}
|
||||
}
|
||||
@@ -144,30 +146,30 @@ const UserList: React.FC = () => {
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
title: t('userList.username') || '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username'
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
title: t('userList.role') || '角色',
|
||||
dataIndex: 'isDefault',
|
||||
key: 'isDefault',
|
||||
width: 100,
|
||||
render: (isDefault: boolean) => (
|
||||
<Tag color={isDefault ? 'red' : 'blue'}>
|
||||
{isDefault ? '默认账户' : '普通用户'}
|
||||
{isDefault ? t('userList.defaultAccount') || '默认账户' : t('userList.normalUser') || '普通用户'}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('common.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180,
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString('zh-CN')
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString(i18n.language || 'zh-CN')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: t('common.actions') || '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render: (_: any, record: User) => {
|
||||
@@ -186,13 +188,13 @@ const UserList: React.FC = () => {
|
||||
setUpdatePasswordModalVisible(true)
|
||||
}}
|
||||
>
|
||||
修改密码
|
||||
{t('userList.updatePassword') || '修改密码'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
title={t('userList.deleteConfirm') || '确定要删除这个用户吗?'}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -200,7 +202,7 @@ const UserList: React.FC = () => {
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
@@ -219,20 +221,20 @@ const UserList: React.FC = () => {
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>用户管理</Title>
|
||||
<Title level={4} style={{ margin: 0 }}>{t('userList.title') || '用户管理'}</Title>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setUpdateOwnPasswordModalVisible(true)}
|
||||
>
|
||||
修改我的密码
|
||||
{t('userList.updateMyPassword') || '修改我的密码'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchUsers}
|
||||
loading={loading}
|
||||
>
|
||||
刷新
|
||||
{t('common.refresh') || '刷新'}
|
||||
</Button>
|
||||
{isDefaultUser && (
|
||||
<Button
|
||||
@@ -240,7 +242,7 @@ const UserList: React.FC = () => {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
>
|
||||
新增用户
|
||||
{t('userList.addUser') || '新增用户'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
@@ -253,7 +255,7 @@ const UserList: React.FC = () => {
|
||||
pagination={{
|
||||
pageSize: isMobile ? 10 : 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
showTotal: (total) => t('userList.total', { total }) || `共 ${total} 条`
|
||||
}}
|
||||
scroll={isMobile ? { x: 600 } : undefined}
|
||||
/>
|
||||
@@ -261,15 +263,15 @@ const UserList: React.FC = () => {
|
||||
|
||||
{/* 创建用户弹窗 */}
|
||||
<Modal
|
||||
title="新增用户"
|
||||
title={t('userList.addUser') || '新增用户'}
|
||||
open={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false)
|
||||
createForm.resetFields()
|
||||
}}
|
||||
onOk={() => createForm.submit()}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
okText={t('userList.createUser') || '创建'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Form
|
||||
form={createForm}
|
||||
@@ -278,29 +280,29 @@ const UserList: React.FC = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
label={t('userList.username') || '用户名'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' }
|
||||
{ required: true, message: t('userList.usernameRequired') || '请输入用户名' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
<Input placeholder={t('userList.usernamePlaceholder') || '请输入用户名'} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
label={t('userList.password') || '密码'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6位' }
|
||||
{ required: true, message: t('userList.passwordRequired') || '请输入密码' },
|
||||
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="至少6位" />
|
||||
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 修改密码弹窗(管理员修改其他用户密码) */}
|
||||
<Modal
|
||||
title="修改密码"
|
||||
title={t('userList.updatePassword') || '修改密码'}
|
||||
open={updatePasswordModalVisible}
|
||||
onCancel={() => {
|
||||
setUpdatePasswordModalVisible(false)
|
||||
@@ -308,8 +310,8 @@ const UserList: React.FC = () => {
|
||||
updatePasswordForm.resetFields()
|
||||
}}
|
||||
onOk={() => updatePasswordForm.submit()}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Form
|
||||
form={updatePasswordForm}
|
||||
@@ -318,28 +320,28 @@ const UserList: React.FC = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
label={t('userList.newPassword') || '新密码'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码至少6位' }
|
||||
{ required: true, message: t('userList.newPasswordRequired') || '请输入新密码' },
|
||||
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="至少6位" />
|
||||
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 修改我的密码弹窗(默认账户修改自己密码) */}
|
||||
<Modal
|
||||
title="修改我的密码"
|
||||
title={t('userList.updateMyPasswordTitle') || '修改我的密码'}
|
||||
open={updateOwnPasswordModalVisible}
|
||||
onCancel={() => {
|
||||
setUpdateOwnPasswordModalVisible(false)
|
||||
updateOwnPasswordForm.resetFields()
|
||||
}}
|
||||
onOk={() => updateOwnPasswordForm.submit()}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Form
|
||||
form={updateOwnPasswordForm}
|
||||
@@ -348,13 +350,13 @@ const UserList: React.FC = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
label={t('userList.newPassword') || '新密码'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码至少6位' }
|
||||
{ required: true, message: t('userList.newPasswordRequired') || '请输入新密码' },
|
||||
{ min: 6, message: t('userList.passwordMinLength') || '密码至少6位' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="至少6位" />
|
||||
<Input.Password placeholder={t('userList.passwordPlaceholder') || '至少6位'} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user