feat: 完成前端多语言支持和菜单优化

- 缩短菜单标题,解决显示不全问题
- 添加跟随系统语言选项,作为默认选项
- 移除语言切换时的页面刷新,实现无刷新切换
- 完成主要页面的多语言替换:
  * ConfigPage - 全局配置页面
  * ResetPassword - 重置密码页面
  * LeaderList - Leader 列表页面
  * UserList - 用户列表页面
  * Statistics - 统计信息页面
  * OrderList - 订单列表页面
  * TemplateList - 模板列表页面
  * CopyTradingList - 跟单配置列表页面
- 添加简体中文、繁体中文和英文的完整翻译键
- 优化语言设置页面,支持跟随系统语言
This commit is contained in:
WrBug
2025-12-04 10:36:28 +08:00
parent 88be178cde
commit 1a9407c544
51 changed files with 4829 additions and 835 deletions
+34 -15
View File
@@ -2,6 +2,9 @@ import { useEffect, useCallback, useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'
import { ConfigProvider, notification, Spin } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import zhTW from 'antd/locale/zh_TW'
import enUS from 'antd/locale/en_US'
import { useTranslation } from 'react-i18next'
import Layout from './components/Layout'
import Login from './pages/Login'
import ResetPassword from './pages/ResetPassword'
@@ -26,6 +29,9 @@ import CopyTradingBuyOrders from './pages/CopyTradingBuyOrders'
import CopyTradingSellOrders from './pages/CopyTradingSellOrders'
import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
import SystemSettings from './pages/SystemSettings'
import LanguageSettings from './pages/LanguageSettings'
import ApiHealthStatus from './pages/ApiHealthStatus'
import ProxySettings from './pages/ProxySettings'
import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
import { apiService } from './services/api'
@@ -50,23 +56,33 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
}
function App() {
const { t, i18n } = useTranslation()
const [isFirstUse, setIsFirstUse] = useState<boolean | null>(null)
const [checking, setChecking] = useState(true)
// 根据当前语言设置 Ant Design 的 locale
const getAntdLocale = () => {
const lang = i18n.language || 'en'
if (lang.startsWith('zh-CN')) return zhCN
if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK')) return zhTW
return enUS
}
/**
* 获取订单类型文本
*/
const getOrderTypeText = useCallback((type: string): string => {
switch (type) {
case 'PLACEMENT':
return '订单创建'
return t('order.create')
case 'UPDATE':
return '订单更新'
return t('order.update')
case 'CANCELLATION':
return '订单取消'
return t('order.cancel')
default:
return '订单事件'
return t('order.event')
}
}, [])
}, [t])
/**
* 处理订单推送消息,显示全局通知
@@ -76,7 +92,7 @@ function App() {
// 根据订单类型和操作类型确定通知内容
const orderTypeText = getOrderTypeText(order.type)
const sideText = order.side === 'BUY' ? '买入' : '卖出'
const sideText = order.side === 'BUY' ? t('order.buy') : t('order.sell')
// 如果有市场名称,在标题中显示
const marketName = orderDetail?.marketName || order.market.substring(0, 8) + '...'
@@ -89,21 +105,21 @@ function App() {
const status = orderDetail?.status || 'UNKNOWN'
// 构建描述信息
let description = `市场: ${marketName}\n${sideText} ${size} @ ${price}`
let description = `${t('order.market')}: ${marketName}\n${sideText} ${size} @ ${price}`
// 如果有订单详情,显示更详细的信息
if (orderDetail) {
description += `\n状态: ${status}`
description += `\n${t('order.status')}: ${status}`
if (parseFloat(filled) > 0) {
description += ` | 已成交: ${filled}`
description += ` | ${t('order.filled')}: ${filled}`
}
const remaining = (parseFloat(size) - parseFloat(filled)).toFixed(2)
if (parseFloat(remaining) > 0) {
description += ` | 剩余: ${remaining}`
description += ` | ${t('order.remaining')}: ${remaining}`
}
} else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) {
// 如果没有订单详情,使用 WebSocket 消息中的已成交数量
description += `\n已成交: ${filled}`
description += `\n${t('order.filled')}: ${filled}`
}
// 根据订单类型选择通知类型
@@ -136,7 +152,7 @@ function App() {
}
} catch (error) {
console.error('检查首次使用失败:', error)
setIsFirstUse(false) // 出错时默认不是首次使用
setIsFirstUse(false)
} finally {
setChecking(false)
}
@@ -170,7 +186,7 @@ function App() {
// 如果正在检查首次使用,显示加载中
if (checking) {
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<div style={{
display: 'flex',
justifyContent: 'center',
@@ -186,7 +202,7 @@ function App() {
// 如果首次使用,直接跳转到重置密码页面
if (isFirstUse === true) {
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<BrowserRouter>
<Routes>
<Route path="/reset-password" element={<ResetPassword />} />
@@ -198,7 +214,7 @@ function App() {
}
return (
<ConfigProvider locale={zhCN}>
<ConfigProvider locale={getAntdLocale()}>
<BrowserRouter>
<Routes>
{/* 公开路由(不需要鉴权) */}
@@ -228,6 +244,9 @@ function App() {
<Route path="/statistics" element={<ProtectedRoute><Statistics /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserList /></ProtectedRoute>} />
<Route path="/system-settings" element={<ProtectedRoute><SystemSettings /></ProtectedRoute>} />
<Route path="/system-settings/language" element={<ProtectedRoute><LanguageSettings /></ProtectedRoute>} />
<Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
<Route path="/system-settings/proxy" element={<ProtectedRoute><ProxySettings /></ProtectedRoute>} />
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react'
import { Select, Space } from 'antd'
import { GlobalOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
const LanguageSwitcher: React.FC = () => {
const { i18n } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [currentLang, setCurrentLang] = useState<string>(i18n.language || 'en')
useEffect(() => {
setCurrentLang(i18n.language || 'en')
}, [i18n.language])
const languages = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
{ value: 'en', label: 'English' }
]
const handleChange = async (value: string) => {
setCurrentLang(value)
await i18n.changeLanguage(value)
// 保存到 localStorage
localStorage.setItem('i18nextLng', value)
// 刷新页面以应用 Ant Design 的 locale 和所有翻译
window.location.reload()
}
return (
<Space>
<GlobalOutlined style={{ color: '#fff', fontSize: isMobile ? '14px' : '16px' }} />
<Select
value={currentLang}
onChange={handleChange}
options={languages}
style={{
width: isMobile ? 100 : 120,
color: '#fff'
}}
dropdownStyle={{
minWidth: 120
}}
bordered={false}
size={isMobile ? 'small' : 'middle'}
/>
</Space>
)
}
export default LanguageSwitcher
+45 -18
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Layout as AntLayout, Menu, Drawer, Button, Modal } from 'antd'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
import {
WalletOutlined,
@@ -15,7 +16,9 @@ import {
LogoutOutlined,
SettingOutlined,
GithubOutlined,
TwitterOutlined
TwitterOutlined,
GlobalOutlined,
CheckCircleOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
@@ -29,6 +32,7 @@ interface LayoutProps {
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const { t } = useTranslation()
const navigate = useNavigate()
const location = useLocation()
const isMobile = useMediaQuery({ maxWidth: 768 })
@@ -46,6 +50,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
}
if (path.startsWith('/system-settings')) {
keys.push('/system-settings')
}
return keys
}
@@ -58,6 +65,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
}
if (path.startsWith('/system-settings')) {
keys.push('/system-settings')
}
setOpenKeys(keys)
}, [location.pathname])
@@ -65,54 +75,71 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
{
key: '/accounts',
icon: <WalletOutlined />,
label: '账户管理'
label: t('menu.accounts')
},
{
key: '/copy-trading-management',
icon: <AppstoreOutlined />,
label: '跟单交易',
label: t('menu.copyTrading'),
children: [
{
key: '/leaders',
icon: <UserOutlined />,
label: 'Leader 管理'
label: t('menu.leaders')
},
{
key: '/templates',
icon: <FileTextOutlined />,
label: '跟单模板'
label: t('menu.templates')
},
{
key: '/copy-trading',
icon: <LinkOutlined />,
label: '跟单配置'
label: t('menu.copyTradingConfig')
}
]
},
{
key: '/positions',
icon: <UnorderedListOutlined />,
label: '仓位管理'
label: t('menu.positions')
},
{
key: '/statistics',
icon: <BarChartOutlined />,
label: '统计信息'
label: t('menu.statistics')
},
{
key: '/users',
icon: <TeamOutlined />,
label: '用户管理'
label: t('menu.users')
},
{
key: '/system-settings',
icon: <SettingOutlined />,
label: '系统管理'
label: t('menu.systemSettings'),
children: [
{
key: '/system-settings/language',
icon: <GlobalOutlined />,
label: t('menu.language')
},
{
key: '/system-settings/api-health',
icon: <CheckCircleOutlined />,
label: t('menu.apiHealth')
},
{
key: '/system-settings/proxy',
icon: <LinkOutlined />,
label: t('menu.proxy')
}
]
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录'
label: t('menu.logout')
}
]
@@ -125,10 +152,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const handleLogoutConfirm = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出登录吗?',
okText: '确定',
cancelText: '取消',
title: t('menu.logoutConfirm'),
content: t('menu.logoutConfirmDesc'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
handleLogout()
if (isMobile) {
@@ -140,7 +167,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const handleMenuClick = ({ key }: { key: string }) => {
// 如果是父菜单,不导航
if (key === '/copy-trading-management') {
if (key === '/copy-trading-management' || key === '/system-settings') {
return
}
@@ -207,7 +234,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
{children}
</Content>
<Drawer
title="导航菜单"
title={t('menu.navigation')}
placement="left"
onClose={() => setMobileMenuOpen(false)}
open={mobileMenuOpen}
@@ -253,7 +280,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
flexShrink: 0
}}>
<span>PolyHermes</span>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<a
href="https://github.com/WrBug/PolyHermes"
target="_blank"
+79
View File
@@ -0,0 +1,79 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import zhCN from '../locales/zh-CN/common.json'
import zhTW from '../locales/zh-TW/common.json'
import en from '../locales/en/common.json'
/**
* 检测系统语言
* 支持的语言:zh-CN, zh-TW, en
* 如果不支持,默认使用 en
*/
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 detectLanguage = (): string => {
// 从 localStorage 读取用户设置的语言
const savedLanguage = localStorage.getItem('i18n_language')
// 如果是 auto 或未设置,使用系统语言
if (!savedLanguage || savedLanguage === 'auto') {
return detectSystemLanguage()
}
// 如果设置了具体语言,使用设置的语言
if (['zh-CN', 'zh-TW', 'en'].includes(savedLanguage)) {
return savedLanguage
}
// 默认使用系统语言
return detectSystemLanguage()
}
i18n
.use(initReactI18next)
.init({
resources: {
'zh-CN': {
translation: zhCN
},
'zh-TW': {
translation: zhTW
},
'en': {
translation: en
}
},
lng: detectLanguage(),
fallbackLng: 'en',
interpolation: {
escapeValue: false // React 已经转义了
}
})
export default i18n
/**
* 切换语言
*/
export const changeLanguage = (lng: 'zh-CN' | 'zh-TW' | 'en') => {
localStorage.setItem('i18n_language', lng)
i18n.changeLanguage(lng)
}
/**
* 获取当前语言
*/
export const getCurrentLanguage = (): string => {
return i18n.language || 'en'
}
+432
View File
@@ -0,0 +1,432 @@
{
"common": {
"back": "Back",
"save": "Save",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"add": "Add",
"search": "Search",
"refresh": "Refresh",
"loading": "Loading...",
"success": "Success",
"failed": "Failed",
"confirm": "Confirm",
"submit": "Submit",
"reset": "Reset",
"close": "Close",
"yes": "Yes",
"no": "No"
},
"account": {
"title": "Account Management",
"list": "Account List",
"detail": "Account Details",
"import": "Import Account",
"update": "Update Account",
"delete": "Delete Account",
"accountId": "Account ID",
"accountName": "Account Name",
"walletAddress": "Wallet Address",
"balance": "Account Balance",
"refreshBalance": "Refresh Balance",
"apiCredentials": "API Credentials Configuration",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "Configured",
"notConfigured": "Not Configured",
"fullConfig": "Full Configuration",
"partialConfig": "Partial Configuration",
"statistics": "Trading Statistics",
"totalOrders": "Total Orders",
"activeOrders": "Active Orders",
"completedOrders": "Completed Orders",
"positionCount": "Position Count",
"totalPnl": "Total P&L",
"editTip": "Edit Tip",
"editTipDesc": "Leave API credential fields empty to keep unchanged. Enter new values to update API credentials.",
"accountNamePlaceholder": "Account Name (Optional)",
"leaveEmptyToNotModify": "Leave empty to not modify",
"updateSuccess": "Account updated successfully",
"updateFailed": "Failed to update account",
"getDetailFailed": "Failed to get account details",
"accountIdRequired": "Account ID cannot be empty"
},
"message": {
"loginSuccess": "Login successful",
"loginFailed": "Login failed",
"createUserSuccess": "User created successfully",
"createUserFailed": "Failed to create user",
"updatePasswordSuccess": "Password updated successfully",
"updatePasswordFailed": "Failed to update password"
},
"login": {
"title": "Login",
"username": "Username",
"password": "Password",
"usernamePlaceholder": "Username",
"passwordPlaceholder": "Password",
"usernameRequired": "Please enter username",
"passwordRequired": "Please enter password",
"forgotPassword": "Forgot password? Reset password"
},
"order": {
"create": "Order Created",
"update": "Order Updated",
"cancel": "Order Cancelled",
"event": "Order Event",
"buy": "Buy",
"sell": "Sell",
"market": "Market",
"status": "Status",
"filled": "Filled",
"remaining": "Remaining"
},
"accountList": {
"title": "Account Management",
"importAccount": "Import Account",
"accountName": "Account Name",
"walletAddress": "Wallet Address",
"proxyAddress": "Proxy Wallet Address",
"apiCredentials": "API Credentials",
"balance": "Balance",
"activeOrders": "Active Orders",
"action": "Actions",
"detail": "Detail",
"edit": "Edit",
"delete": "Delete",
"viewDetail": "View Detail",
"deleteConfirm": "Are you sure you want to delete this account?",
"deleteConfirmDesc": "Before deleting the account, please make sure all active orders are cancelled. This action cannot be undone!",
"deleteConfirmDescSimple": "This action cannot be undone!",
"deleteConfirmOk": "Confirm Delete",
"deleteSuccess": "Account deleted successfully",
"deleteFailed": "Failed to delete account",
"copySuccess": "Copied to clipboard",
"copyFailed": "Copy failed",
"fullConfig": "Full Config",
"partialConfig": "Partial Config",
"notConfigured": "Not Configured",
"totalBalance": "Total Balance",
"available": "Available",
"position": "Position",
"refreshBalance": "Refresh Balance",
"refreshBalanceSuccess": "Balance refreshed successfully",
"refreshBalanceFailed": "Failed to refresh balance",
"getDetailFailed": "Failed to get account detail",
"openDetailFailed": "Failed to open detail",
"accountDetail": "Account Detail",
"accountId": "Account ID",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "Configured",
"notConfiguredStatus": "Not Configured",
"configStatus": "Config Status",
"statistics": "Trading Statistics",
"totalOrders": "Total Orders",
"activeOrdersCount": "Active Orders",
"completedOrders": "Completed Orders",
"positionCount": "Position Count",
"totalPnl": "Total PnL",
"editAccount": "Edit Account",
"editTip": "Edit Tip",
"editTipDesc": "Leave API credential fields empty to keep unchanged. Enter new values to update API credentials.",
"accountNamePlaceholder": "Account Name (Optional)",
"leaveEmptyToNotModify": "Leave empty to keep unchanged",
"updateSuccess": "Account updated successfully",
"updateFailed": "Failed to update account",
"getDetailFailedForEdit": "Failed to get account detail",
"loading": "Loading..."
},
"accountImport": {
"title": "Import Account",
"back": "Back",
"securityTip": "Security Tip",
"securityTipDesc": "Private keys will be stored in the backend database. Please ensure database access is secure. HTTPS is recommended.",
"importMethod": "Import Method",
"privateKey": "Private Key",
"mnemonic": "Mnemonic",
"privateKeyLabel": "Private Key",
"privateKeyPlaceholder": "Enter private key (64-character hex string, 0x prefix optional)",
"privateKeyRequired": "Please enter private key",
"privateKeyInvalid": "Invalid private key format (should be 64-character hex string)",
"walletAddress": "Wallet Address",
"walletAddressPlaceholder": "Wallet address (will be derived from private key)",
"walletAddressRequired": "Please enter wallet address",
"walletAddressInvalid": "Invalid wallet address format",
"walletAddressMismatch": "Wallet address does not match private key",
"mnemonicLabel": "Mnemonic",
"mnemonicPlaceholder": "Enter 12 or 24 words mnemonic (space-separated)",
"mnemonicRequired": "Please enter mnemonic",
"mnemonicInvalid": "Invalid mnemonic format (should be 12 or 24 words, space-separated)",
"walletAddressMismatchMnemonic": "Wallet address does not match mnemonic",
"accountName": "Account Name",
"accountNamePlaceholder": "Optional, for identifying account",
"importAccount": "Import Account",
"importSuccess": "Account imported successfully",
"importFailed": "Failed to import account",
"derivedAddress": "Derived Address",
"addressError": "Cannot derive address from private key",
"addressErrorMnemonic": "Cannot derive address from mnemonic"
},
"leader": {
"title": "Leader Management",
"leaderName": "Leader Name",
"walletAddress": "Wallet Address",
"category": "Category",
"all": "All",
"copyTradingCount": "Copy Trading Count",
"createdAt": "Created At",
"action": "Actions",
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"listFailed": "Failed to get Leader list",
"deleteSuccess": "Leader deleted successfully",
"deleteFailed": "Failed to delete Leader",
"deleteConfirm": "Are you sure you want to delete this Leader?",
"deleteConfirmDesc": "This action cannot be undone!",
"deleteConfirmOk": "Confirm Delete"
},
"menu": {
"accounts": "Account Management",
"copyTrading": "Copy Trading",
"leaders": "Leader Management",
"templates": "Templates",
"copyTradingConfig": "Copy Trading Config",
"positions": "Position Management",
"statistics": "Statistics",
"users": "User Management",
"systemSettings": "System",
"language": "Language",
"apiHealth": "API Health",
"proxy": "Proxy",
"logout": "Logout",
"logoutConfirm": "Confirm Logout",
"logoutConfirmDesc": "Are you sure you want to logout?",
"navigation": "Navigation Menu"
},
"languageSettings": {
"title": "Language Settings",
"currentLanguage": "Current Language",
"followSystem": "Follow System",
"currentSystemLanguage": "Current System Language",
"description": "After changing the language, the interface will update immediately.",
"changeSuccess": "Language changed successfully",
"changeFailed": "Failed to change language"
},
"apiHealthStatus": {
"title": "API Health Status",
"normal": "Normal",
"notConfigured": "Not Configured",
"abnormal": "Abnormal",
"responseTime": "Response Time"
},
"proxySettings": {
"title": "Proxy Settings",
"enabled": "Enable Proxy",
"host": "Proxy Host",
"port": "Proxy Port",
"username": "Proxy Username (Optional)",
"password": "Proxy Password (Optional)",
"hostRequired": "Please enter proxy host address",
"hostInvalid": "Please enter a valid host address",
"hostPlaceholder": "e.g., 127.0.0.1 or proxy.example.com",
"portRequired": "Please enter proxy port",
"portInvalid": "Port must be between 1-65535",
"portPlaceholder": "e.g., 8888",
"usernamePlaceholder": "Enter username if proxy requires authentication",
"passwordPlaceholder": "Enter password if proxy requires authentication",
"passwordPlaceholderUpdate": "Leave empty to keep password unchanged",
"passwordHelp": "Enter password if proxy requires authentication",
"passwordHelpUpdate": "Leave empty to keep password unchanged, enter new password to update",
"check": "Check Proxy",
"checkSuccess": "Proxy check successful",
"checkFailed": "Proxy check failed",
"saveSuccess": "Configuration saved successfully",
"saveFailed": "Failed to save configuration",
"getFailed": "Failed to get proxy configuration",
"latency": "Latency"
},
"configPage": {
"title": "Global Configuration",
"message": "Configuration Function Migrated",
"description": "Global configuration function has been migrated to the following pages:",
"templates": "Templates",
"templatesDesc": "Manage copy trading parameters (ratio, amount, risk control, etc.)",
"copyTrading": "Copy Trading Config",
"copyTradingDesc": "Associate accounts, templates and Leaders to enable copy trading relationships",
"systemSettings": "System Settings",
"systemSettingsDesc": "Configure proxy, view API health status",
"footer": "Please use the above pages for configuration management."
},
"resetPassword": {
"title": "Reset Password",
"firstUse": "First Time Using System",
"firstUseDesc": "Please use the reset key provided by the administrator to set the initial password",
"resetKey": "Reset Key",
"resetKeyRequired": "Please enter reset key",
"resetKeyPlaceholder": "Please enter reset key",
"username": "Username",
"usernameRequired": "Please enter username",
"usernamePlaceholder": "Please enter username",
"newPassword": "New Password",
"newPasswordRequired": "Please enter new password",
"passwordPlaceholder": "At least 6 characters",
"passwordMinLength": "Password must be at least 6 characters",
"passwordStrength": "Password Strength",
"weak": "Weak",
"fair": "Fair",
"medium": "Medium",
"strong": "Strong",
"veryStrong": "Very Strong",
"confirmPassword": "Confirm Password",
"confirmPasswordRequired": "Please confirm password",
"confirmPasswordPlaceholder": "Please enter password again",
"passwordMismatch": "Passwords do not match",
"submit": "Reset Password",
"success": "Password reset successfully",
"failed": "Password reset failed"
},
"leaderList": {
"title": "Leader Management",
"addLeader": "Add Leader",
"leaderName": "Leader Name",
"walletAddress": "Wallet Address",
"category": "Category",
"all": "All",
"copyTradingCount": "Copy Trading Count",
"copyTradingRelations": "{{count}} copy trading relations",
"createdAt": "Created At",
"noData": "No Leader data",
"fetchFailed": "Failed to get Leader list",
"deleteSuccess": "Leader deleted successfully",
"deleteFailed": "Failed to delete Leader",
"deleteConfirm": "Are you sure you want to delete this Leader?",
"deleteConfirmDesc": "This Leader has {{count}} copy trading relations, please delete them first"
},
"userList": {
"title": "User Management",
"username": "Username",
"role": "Role",
"defaultAccount": "Default Account",
"normalUser": "Normal User",
"updateMyPassword": "Update My Password",
"addUser": "Add User",
"createUser": "Create User",
"updatePassword": "Update Password",
"updateMyPasswordTitle": "Update My Password",
"newPassword": "New Password",
"password": "Password",
"passwordRequired": "Please enter password",
"passwordMinLength": "Password must be at least 6 characters",
"passwordPlaceholder": "At least 6 characters",
"usernameRequired": "Please enter username",
"usernamePlaceholder": "Please enter username",
"newPasswordRequired": "Please enter new password",
"fetchFailed": "Failed to get user list",
"createSuccess": "User created successfully",
"createFailed": "Failed to create user",
"updatePasswordSuccess": "Password updated successfully",
"updatePasswordFailed": "Failed to update password",
"updateOwnPasswordSuccess": "Password updated successfully, please login again",
"updateOwnPasswordFailed": "Failed to update password",
"deleteSuccess": "User deleted successfully",
"deleteFailed": "Failed to delete user",
"deleteConfirm": "Are you sure you want to delete this user?",
"total": "Total {{total}} items"
},
"statistics": {
"title": "Statistics",
"totalOrders": "Total Orders",
"totalPnl": "Total P&L",
"winRate": "Win Rate",
"avgPnl": "Average P&L",
"maxProfit": "Max Profit",
"maxLoss": "Max Loss",
"startDate": "Start Date",
"endDate": "End Date",
"fetchFailed": "Failed to get statistics",
"refresh": "Refresh",
"reset": "Reset"
},
"orderList": {
"title": "Order Management",
"leader": "Leader",
"market": "Market",
"category": "Category",
"side": "Side",
"price": "Price",
"size": "Size",
"status": "Status",
"pnl": "P&L",
"createdAt": "Created At",
"fetchFailed": "Failed to get order list"
},
"templateList": {
"title": "Template Management",
"addTemplate": "Add Template",
"searchPlaceholder": "Search template name",
"templateName": "Template Name",
"copyMode": "Copy Mode",
"ratio": "Ratio",
"fixedAmount": "Fixed Amount",
"ratioMode": "Ratio Mode",
"fixedAmountMode": "Fixed Amount Mode",
"copyConfig": "Copy Config",
"supportSell": "Support Sell",
"notSupportSell": "Not Support Sell",
"useCount": "Use Count",
"timesUsed": " times used",
"amountLimit": "Amount Limit",
"max": "Max",
"min": "Min",
"notSet": "Not Set",
"otherConfig": "Other Config",
"maxDailyOrders": "Max Daily Orders",
"priceTolerance": "Price Tolerance",
"copy": "Copy",
"copySuffix": "Copy",
"noData": "No template data",
"fetchFailed": "Failed to get template list",
"deleteSuccess": "Template deleted successfully",
"deleteFailed": "Failed to delete template",
"deleteConfirm": "Are you sure you want to delete this template?",
"deleteConfirmDesc": "This action cannot be undone. Please ensure no copy trading relationships are using this template",
"copySuccess": "Template copied successfully",
"copyFailed": "Failed to copy template",
"minAmountError": "Minimum amount must be >= 1",
"fixedAmountRequired": "Please enter fixed copy trading amount",
"invalidNumber": "Please enter a valid number",
"fixedAmountError": "Fixed amount must be >= 1, please re-enter"
},
"copyTradingList": {
"title": "Copy Trading Config Management",
"addCopyTrading": "Add Copy Trading",
"wallet": "Wallet",
"account": "Account",
"template": "Template",
"leader": "Leader",
"enabled": "Enabled",
"disabled": "Disabled",
"totalPnl": "Total P&L",
"statistics": "Statistics",
"orders": "Orders",
"viewStatistics": "View Statistics",
"buyOrders": "Buy Orders",
"sellOrders": "Sell Orders",
"matchedOrders": "Matched Orders",
"filterWallet": "Filter Wallet",
"filterTemplate": "Filter Template",
"filterLeader": "Filter Leader",
"fetchFailed": "Failed to get copy trading list",
"startSuccess": "Copy trading started successfully",
"stopSuccess": "Copy trading stopped successfully",
"updateStatusFailed": "Failed to update copy trading status",
"deleteSuccess": "Copy trading deleted successfully",
"deleteFailed": "Failed to delete copy trading",
"deleteConfirm": "Are you sure you want to delete this copy trading relationship?"
}
}
+334
View File
@@ -0,0 +1,334 @@
{
"common": {
"save": "保存",
"cancel": "取消",
"confirm": "确定",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"refresh": "刷新",
"search": "搜索",
"reset": "重置",
"submit": "提交",
"actions": "操作",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"status": "状态",
"enabled": "启用",
"disabled": "禁用",
"yes": "是",
"no": "否",
"all": "全部",
"loading": "加载中",
"noData": "暂无数据",
"saveConfig": "保存配置",
"refreshConfig": "刷新配置"
},
"login": {
"title": "登录",
"username": "用户名",
"password": "密码",
"usernameRequired": "请输入用户名",
"passwordRequired": "请输入密码",
"usernamePlaceholder": "请输入用户名",
"passwordPlaceholder": "请输入密码",
"forgotPassword": "忘记密码?",
"loginFailed": "登录失败",
"loginSuccess": "登录成功"
},
"message": {
"success": "操作成功",
"error": "操作失败",
"loading": "加载中...",
"noData": "暂无数据"
},
"order": {
"create": "创建订单",
"update": "更新订单",
"cancel": "取消订单",
"event": "订单事件",
"buy": "买入",
"sell": "卖出"
},
"accountList": {
"title": "账户管理",
"addAccount": "添加账户",
"importAccount": "导入账户",
"accountName": "账户名称",
"walletAddress": "钱包地址",
"balance": "余额",
"actions": "操作",
"edit": "编辑",
"delete": "删除",
"viewDetail": "查看详情",
"deleteConfirm": "确定要删除这个账户吗?",
"deleteSuccess": "删除账户成功",
"deleteFailed": "删除账户失败",
"fetchFailed": "获取账户列表失败"
},
"accountImport": {
"title": "导入账户",
"privateKey": "私钥",
"privateKeyRequired": "请输入私钥",
"privateKeyPlaceholder": "请输入或粘贴私钥",
"privateKeyHelp": "私钥将加密存储,仅用于签名交易",
"accountName": "账户名称",
"accountNameRequired": "请输入账户名称",
"accountNamePlaceholder": "请输入账户名称",
"accountNameHelp": "用于标识账户,便于管理",
"submit": "导入",
"importSuccess": "导入账户成功",
"importFailed": "导入账户失败",
"invalidPrivateKey": "无效的私钥",
"duplicateAccount": "账户已存在"
},
"leader": {
"title": "Leader 管理",
"leaderName": "Leader 名称",
"leaderAddress": "钱包地址",
"category": "分类",
"addLeader": "添加 Leader",
"editLeader": "编辑 Leader",
"deleteLeader": "删除 Leader"
},
"menu": {
"accounts": "账户管理",
"copyTrading": "跟单交易",
"leaders": "Leader 管理",
"templates": "跟单模板",
"copyTradingConfig": "跟单配置",
"positions": "仓位管理",
"statistics": "统计信息",
"users": "用户管理",
"systemSettings": "系统管理",
"language": "语言",
"apiHealth": "API健康",
"proxy": "代理",
"logout": "退出登录",
"logoutConfirm": "确认退出",
"logoutConfirmDesc": "确定要退出登录吗?",
"navigation": "导航菜单"
},
"apiHealthStatus": {
"title": "API 健康状态",
"checkFailed": "检查失败",
"status": "状态",
"responseTime": "响应时间",
"lastCheck": "最后检查",
"healthy": "健康",
"unhealthy": "不健康",
"unknown": "未知"
},
"proxySettings": {
"title": "代理设置",
"enabled": "启用代理",
"host": "代理主机",
"port": "代理端口",
"username": "用户名",
"password": "密码",
"hostRequired": "请输入代理主机",
"portRequired": "请输入代理端口",
"hostPlaceholder": "例如:127.0.0.1",
"portPlaceholder": "例如:8080",
"usernamePlaceholder": "如果代理需要认证,请输入用户名",
"passwordPlaceholder": "如果代理需要认证,请输入密码",
"passwordPlaceholderUpdate": "留空则不更新密码",
"passwordHelp": "如果代理需要认证,请输入密码",
"passwordHelpUpdate": "留空则不更新密码,输入新密码则更新",
"check": "检查代理",
"checkSuccess": "代理检查成功",
"checkFailed": "代理检查失败",
"saveSuccess": "保存配置成功",
"saveFailed": "保存配置失败",
"getFailed": "获取代理配置失败",
"latency": "延迟"
},
"languageSettings": {
"title": "语言设置",
"currentLanguage": "当前语言",
"followSystem": "跟随系统",
"currentSystemLanguage": "当前系统语言",
"description": "切换语言后,界面将立即更新为新语言。",
"changeSuccess": "语言切换成功",
"changeFailed": "语言切换失败"
},
"configPage": {
"title": "全局配置",
"message": "配置功能已迁移",
"description": "全局配置功能已迁移到以下页面:",
"templates": "跟单模板",
"templatesDesc": "管理跟单参数(比例、金额、风险控制等)",
"copyTrading": "跟单配置",
"copyTradingDesc": "将账户、模板和 Leader 关联,启用跟单关系",
"systemSettings": "系统管理",
"systemSettingsDesc": "配置代理、查看 API 健康状态",
"footer": "请使用上述页面进行配置管理。"
},
"resetPassword": {
"title": "重置密码",
"firstUse": "首次使用系统",
"firstUseDesc": "请使用管理员提供的重置密钥设置初始密码",
"resetKey": "重置密钥",
"resetKeyRequired": "请输入重置密钥",
"resetKeyPlaceholder": "请输入重置密钥",
"username": "用户名",
"usernameRequired": "请输入用户名",
"usernamePlaceholder": "请输入用户名",
"newPassword": "新密码",
"newPasswordRequired": "请输入新密码",
"passwordPlaceholder": "至少6位",
"passwordMinLength": "密码至少6位",
"passwordStrength": "密码强度",
"weak": "弱",
"fair": "较弱",
"medium": "中等",
"strong": "强",
"veryStrong": "很强",
"confirmPassword": "确认密码",
"confirmPasswordRequired": "请确认密码",
"confirmPasswordPlaceholder": "请再次输入密码",
"passwordMismatch": "两次输入的密码不一致",
"submit": "重置密码",
"success": "密码重置成功",
"failed": "密码重置失败"
},
"leaderList": {
"title": "Leader 管理",
"addLeader": "添加 Leader",
"leaderName": "Leader 名称",
"walletAddress": "钱包地址",
"category": "分类",
"all": "全部",
"copyTradingCount": "跟单关系数",
"copyTradingRelations": "{{count}} 个跟单关系",
"createdAt": "创建时间",
"noData": "暂无 Leader 数据",
"fetchFailed": "获取 Leader 列表失败",
"deleteSuccess": "删除 Leader 成功",
"deleteFailed": "删除 Leader 失败",
"deleteConfirm": "确定要删除这个 Leader 吗?",
"deleteConfirmDesc": "该 Leader 还有 {{count}} 个跟单关系,请先删除跟单关系"
},
"userList": {
"title": "用户管理",
"username": "用户名",
"role": "角色",
"defaultAccount": "默认账户",
"normalUser": "普通用户",
"updateMyPassword": "修改我的密码",
"addUser": "新增用户",
"createUser": "创建用户",
"updatePassword": "修改密码",
"updateMyPasswordTitle": "修改我的密码",
"newPassword": "新密码",
"password": "密码",
"passwordRequired": "请输入密码",
"passwordMinLength": "密码至少6位",
"passwordPlaceholder": "至少6位",
"usernameRequired": "请输入用户名",
"usernamePlaceholder": "请输入用户名",
"newPasswordRequired": "请输入新密码",
"fetchFailed": "获取用户列表失败",
"createSuccess": "创建用户成功",
"createFailed": "创建用户失败",
"updatePasswordSuccess": "更新密码成功",
"updatePasswordFailed": "更新密码失败",
"updateOwnPasswordSuccess": "修改密码成功,请重新登录",
"updateOwnPasswordFailed": "修改密码失败",
"deleteSuccess": "删除用户成功",
"deleteFailed": "删除用户失败",
"deleteConfirm": "确定要删除这个用户吗?",
"total": "共 {{total}} 条"
},
"statistics": {
"title": "统计信息",
"totalOrders": "总订单数",
"totalPnl": "总盈亏",
"winRate": "胜率",
"avgPnl": "平均盈亏",
"maxProfit": "最大盈利",
"maxLoss": "最大亏损",
"startDate": "开始日期",
"endDate": "结束日期",
"fetchFailed": "获取统计信息失败",
"refresh": "刷新",
"reset": "重置"
},
"orderList": {
"title": "订单管理",
"leader": "Leader",
"market": "市场",
"category": "分类",
"side": "方向",
"price": "价格",
"size": "数量",
"status": "状态",
"pnl": "盈亏",
"createdAt": "创建时间",
"fetchFailed": "获取订单列表失败"
},
"templateList": {
"title": "跟单模板管理",
"addTemplate": "新增模板",
"searchPlaceholder": "搜索模板名称",
"templateName": "模板名称",
"copyMode": "跟单模式",
"ratio": "比例",
"fixedAmount": "固定金额",
"ratioMode": "比例模式",
"fixedAmountMode": "固定金额模式",
"copyConfig": "跟单配置",
"supportSell": "跟单卖出",
"notSupportSell": "不跟单卖出",
"useCount": "使用次数",
"timesUsed": "次使用",
"amountLimit": "金额限制",
"max": "最大",
"min": "最小",
"notSet": "未设置",
"otherConfig": "其他配置",
"maxDailyOrders": "每日最大订单",
"priceTolerance": "价格容忍度",
"copy": "复制",
"copySuffix": "副本",
"noData": "暂无模板数据",
"fetchFailed": "获取模板列表失败",
"deleteSuccess": "删除模板成功",
"deleteFailed": "删除模板失败",
"deleteConfirm": "确定要删除这个模板吗?",
"deleteConfirmDesc": "删除后无法恢复,请确保没有跟单关系在使用该模板",
"copySuccess": "复制模板成功",
"copyFailed": "复制模板失败",
"minAmountError": "最小金额必须 >= 1",
"fixedAmountRequired": "请输入固定跟单金额",
"invalidNumber": "请输入有效的数字",
"fixedAmountError": "固定金额必须 >= 1,请重新输入"
},
"copyTradingList": {
"title": "跟单配置管理",
"addCopyTrading": "新增跟单",
"wallet": "钱包",
"account": "账户",
"template": "模板",
"leader": "Leader",
"enabled": "开启",
"disabled": "停止",
"totalPnl": "总盈亏",
"statistics": "统计",
"orders": "订单",
"viewStatistics": "查看统计",
"buyOrders": "买入订单",
"sellOrders": "卖出订单",
"matchedOrders": "匹配关系",
"filterWallet": "筛选钱包",
"filterTemplate": "筛选模板",
"filterLeader": "筛选 Leader",
"fetchFailed": "获取跟单列表失败",
"startSuccess": "开启跟单成功",
"stopSuccess": "停止跟单成功",
"updateStatusFailed": "更新跟单状态失败",
"deleteSuccess": "删除跟单成功",
"deleteFailed": "删除跟单失败",
"deleteConfirm": "确定要删除这个跟单关系吗?"
}
}
+432
View File
@@ -0,0 +1,432 @@
{
"common": {
"back": "返回",
"save": "保存",
"cancel": "取消",
"edit": "編輯",
"delete": "刪除",
"add": "添加",
"search": "搜索",
"refresh": "刷新",
"loading": "加載中...",
"success": "成功",
"failed": "失敗",
"confirm": "確認",
"submit": "提交",
"reset": "重置",
"close": "關閉",
"yes": "是",
"no": "否"
},
"account": {
"title": "賬戶管理",
"list": "賬戶列表",
"detail": "賬戶詳情",
"import": "導入賬戶",
"update": "更新賬戶",
"delete": "刪除賬戶",
"accountId": "賬戶ID",
"accountName": "賬戶名稱",
"walletAddress": "錢包地址",
"balance": "賬戶餘額",
"refreshBalance": "刷新餘額",
"apiCredentials": "API 憑證配置",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "已配置",
"notConfigured": "未配置",
"fullConfig": "完整配置",
"partialConfig": "部分配置",
"statistics": "交易統計",
"totalOrders": "總訂單數",
"activeOrders": "活躍訂單數",
"completedOrders": "已完成訂單數",
"positionCount": "持倉數量",
"totalPnl": "總盈虧",
"editTip": "編輯提示",
"editTipDesc": "API 憑證字段留空表示不修改。如需更新 API 憑證,請輸入新值;如需保持原值不變,請留空。",
"accountNamePlaceholder": "賬戶名稱(可選)",
"leaveEmptyToNotModify": "留空表示不修改",
"updateSuccess": "更新賬戶成功",
"updateFailed": "更新賬戶失敗",
"getDetailFailed": "獲取賬戶詳情失敗",
"accountIdRequired": "賬戶ID不能為空"
},
"message": {
"loginSuccess": "登錄成功",
"loginFailed": "登錄失敗",
"createUserSuccess": "創建用戶成功",
"createUserFailed": "創建用戶失敗",
"updatePasswordSuccess": "更新密碼成功",
"updatePasswordFailed": "更新密碼失敗"
},
"login": {
"title": "登錄",
"username": "用戶名",
"password": "密碼",
"usernamePlaceholder": "用戶名",
"passwordPlaceholder": "密碼",
"usernameRequired": "請輸入用戶名",
"passwordRequired": "請輸入密碼",
"forgotPassword": "忘記密碼?重置密碼"
},
"order": {
"create": "訂單創建",
"update": "訂單更新",
"cancel": "訂單取消",
"event": "訂單事件",
"buy": "買入",
"sell": "賣出",
"market": "市場",
"status": "狀態",
"filled": "已成交",
"remaining": "剩餘"
},
"accountList": {
"title": "賬戶管理",
"importAccount": "導入賬戶",
"accountName": "賬戶名稱",
"walletAddress": "錢包地址",
"proxyAddress": "代理錢包地址",
"apiCredentials": "API 憑證",
"balance": "餘額",
"activeOrders": "活躍訂單",
"action": "操作",
"detail": "詳情",
"edit": "編輯",
"delete": "刪除",
"viewDetail": "查看詳情",
"deleteConfirm": "確定要刪除這個賬戶嗎?",
"deleteConfirmDesc": "刪除賬戶前,請確保已取消所有活躍訂單。刪除後無法恢復,請謹慎操作!",
"deleteConfirmDescSimple": "刪除後無法恢復,請謹慎操作!",
"deleteConfirmOk": "確定刪除",
"deleteSuccess": "刪除賬戶成功",
"deleteFailed": "刪除賬戶失敗",
"copySuccess": "已複製到剪貼板",
"copyFailed": "複製失敗",
"fullConfig": "完整配置",
"partialConfig": "部分配置",
"notConfigured": "未配置",
"totalBalance": "總餘額",
"available": "可用",
"position": "倉位",
"refreshBalance": "刷新餘額",
"refreshBalanceSuccess": "餘額刷新成功",
"refreshBalanceFailed": "刷新餘額失敗",
"getDetailFailed": "獲取賬戶詳情失敗",
"openDetailFailed": "打開詳情失敗",
"accountDetail": "賬戶詳情",
"accountId": "賬戶ID",
"apiKey": "API Key",
"apiSecret": "API Secret",
"apiPassphrase": "API Passphrase",
"configured": "已配置",
"notConfiguredStatus": "未配置",
"configStatus": "配置狀態",
"statistics": "交易統計",
"totalOrders": "總訂單數",
"activeOrdersCount": "活躍訂單數",
"completedOrders": "已完成訂單數",
"positionCount": "持倉數量",
"totalPnl": "總盈虧",
"editAccount": "編輯賬戶",
"editTip": "編輯提示",
"editTipDesc": "API 憑證字段留空表示不修改。如需更新 API 憑證,請輸入新值;如需保持原值不變,請留空。",
"accountNamePlaceholder": "賬戶名稱(可選)",
"leaveEmptyToNotModify": "留空表示不修改",
"updateSuccess": "更新賬戶成功",
"updateFailed": "更新賬戶失敗",
"getDetailFailedForEdit": "獲取賬戶詳情失敗",
"loading": "加載中..."
},
"accountImport": {
"title": "導入賬戶",
"back": "返回",
"securityTip": "安全提示",
"securityTipDesc": "私鑰將存儲在後端數據庫中,請確保數據庫訪問安全。建議使用 HTTPS 連接。",
"importMethod": "導入方式",
"privateKey": "私鑰",
"mnemonic": "助記詞",
"privateKeyLabel": "私鑰",
"privateKeyPlaceholder": "請輸入私鑰(64位十六進制字符串,可選0x前綴)",
"privateKeyRequired": "請輸入私鑰",
"privateKeyInvalid": "私鑰格式不正確(應為64位十六進制字符串)",
"walletAddress": "錢包地址",
"walletAddressPlaceholder": "錢包地址(將從私鑰自動推導)",
"walletAddressRequired": "請輸入錢包地址",
"walletAddressInvalid": "錢包地址格式不正確",
"walletAddressMismatch": "錢包地址與私鑰不匹配",
"mnemonicLabel": "助記詞",
"mnemonicPlaceholder": "請輸入12或24個單詞的助記詞(用空格分隔)",
"mnemonicRequired": "請輸入助記詞",
"mnemonicInvalid": "助記詞格式不正確(應為12或24個單詞,用空格分隔)",
"walletAddressMismatchMnemonic": "錢包地址與助記詞不匹配",
"accountName": "賬戶名稱",
"accountNamePlaceholder": "可選,用於標識賬戶",
"importAccount": "導入賬戶",
"importSuccess": "導入賬戶成功",
"importFailed": "導入賬戶失敗",
"derivedAddress": "推導地址",
"addressError": "無法從私鑰推導地址",
"addressErrorMnemonic": "無法從助記詞推導地址"
},
"leader": {
"title": "Leader 管理",
"leaderName": "Leader 名稱",
"walletAddress": "錢包地址",
"category": "分類",
"all": "全部",
"copyTradingCount": "跟單關係數",
"createdAt": "創建時間",
"action": "操作",
"add": "添加",
"edit": "編輯",
"delete": "刪除",
"listFailed": "獲取 Leader 列表失敗",
"deleteSuccess": "刪除 Leader 成功",
"deleteFailed": "刪除 Leader 失敗",
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
"deleteConfirmDesc": "刪除後無法恢復,請謹慎操作!",
"deleteConfirmOk": "確定刪除"
},
"menu": {
"accounts": "賬戶管理",
"copyTrading": "跟單交易",
"leaders": "Leader 管理",
"templates": "跟單模板",
"copyTradingConfig": "跟單配置",
"positions": "倉位管理",
"statistics": "統計信息",
"users": "用戶管理",
"systemSettings": "系統管理",
"language": "語言",
"apiHealth": "API健康",
"proxy": "代理",
"logout": "退出登錄",
"logoutConfirm": "確認退出",
"logoutConfirmDesc": "確定要退出登錄嗎?",
"navigation": "導航菜單"
},
"languageSettings": {
"title": "語言設置",
"currentLanguage": "當前語言",
"followSystem": "跟隨系統",
"currentSystemLanguage": "當前系統語言",
"description": "切換語言後,界面將立即更新為新語言。",
"changeSuccess": "語言切換成功",
"changeFailed": "語言切換失敗"
},
"apiHealthStatus": {
"title": "API 健康狀態",
"normal": "正常",
"notConfigured": "未配置",
"abnormal": "異常",
"responseTime": "響應時間"
},
"proxySettings": {
"title": "代理設置",
"enabled": "啟用代理",
"host": "代理主機",
"port": "代理端口",
"username": "代理用戶名(可選)",
"password": "代理密碼(可選)",
"hostRequired": "請輸入代理主機地址",
"hostInvalid": "請輸入有效的主機地址",
"hostPlaceholder": "例如:127.0.0.1 或 proxy.example.com",
"portRequired": "請輸入代理端口",
"portInvalid": "端口必須在 1-65535 之間",
"portPlaceholder": "例如:8888",
"usernamePlaceholder": "如果代理需要認證,請輸入用戶名",
"passwordPlaceholder": "如果代理需要認證,請輸入密碼",
"passwordPlaceholderUpdate": "留空則不更新密碼",
"passwordHelp": "如果代理需要認證,請輸入密碼",
"passwordHelpUpdate": "留空則不更新密碼,輸入新密碼則更新",
"check": "檢查代理",
"checkSuccess": "代理檢查成功",
"checkFailed": "代理檢查失敗",
"saveSuccess": "保存配置成功",
"saveFailed": "保存配置失敗",
"getFailed": "獲取代理配置失敗",
"latency": "延遲"
},
"configPage": {
"title": "全局配置",
"message": "配置功能已遷移",
"description": "全局配置功能已遷移到以下頁面:",
"templates": "跟單模板",
"templatesDesc": "管理跟單參數(比例、金額、風險控制等)",
"copyTrading": "跟單配置",
"copyTradingDesc": "將賬戶、模板和 Leader 關聯,啟用跟單關係",
"systemSettings": "系統管理",
"systemSettingsDesc": "配置代理、查看 API 健康狀態",
"footer": "請使用上述頁面進行配置管理。"
},
"resetPassword": {
"title": "重置密碼",
"firstUse": "首次使用系統",
"firstUseDesc": "請使用管理員提供的重置密鑰設置初始密碼",
"resetKey": "重置密鑰",
"resetKeyRequired": "請輸入重置密鑰",
"resetKeyPlaceholder": "請輸入重置密鑰",
"username": "用戶名",
"usernameRequired": "請輸入用戶名",
"usernamePlaceholder": "請輸入用戶名",
"newPassword": "新密碼",
"newPasswordRequired": "請輸入新密碼",
"passwordPlaceholder": "至少6位",
"passwordMinLength": "密碼至少6位",
"passwordStrength": "密碼強度",
"weak": "弱",
"fair": "較弱",
"medium": "中等",
"strong": "強",
"veryStrong": "很強",
"confirmPassword": "確認密碼",
"confirmPasswordRequired": "請確認密碼",
"confirmPasswordPlaceholder": "請再次輸入密碼",
"passwordMismatch": "兩次輸入的密碼不一致",
"submit": "重置密碼",
"success": "密碼重置成功",
"failed": "密碼重置失敗"
},
"leaderList": {
"title": "Leader 管理",
"addLeader": "添加 Leader",
"leaderName": "Leader 名稱",
"walletAddress": "錢包地址",
"category": "分類",
"all": "全部",
"copyTradingCount": "跟單關係數",
"copyTradingRelations": "{{count}} 個跟單關係",
"createdAt": "創建時間",
"noData": "暫無 Leader 數據",
"fetchFailed": "獲取 Leader 列表失敗",
"deleteSuccess": "刪除 Leader 成功",
"deleteFailed": "刪除 Leader 失敗",
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
"deleteConfirmDesc": "該 Leader 還有 {{count}} 個跟單關係,請先刪除跟單關係"
},
"userList": {
"title": "用戶管理",
"username": "用戶名",
"role": "角色",
"defaultAccount": "默認賬戶",
"normalUser": "普通用戶",
"updateMyPassword": "修改我的密碼",
"addUser": "新增用戶",
"createUser": "創建用戶",
"updatePassword": "修改密碼",
"updateMyPasswordTitle": "修改我的密碼",
"newPassword": "新密碼",
"password": "密碼",
"passwordRequired": "請輸入密碼",
"passwordMinLength": "密碼至少6位",
"passwordPlaceholder": "至少6位",
"usernameRequired": "請輸入用戶名",
"usernamePlaceholder": "請輸入用戶名",
"newPasswordRequired": "請輸入新密碼",
"fetchFailed": "獲取用戶列表失敗",
"createSuccess": "創建用戶成功",
"createFailed": "創建用戶失敗",
"updatePasswordSuccess": "更新密碼成功",
"updatePasswordFailed": "更新密碼失敗",
"updateOwnPasswordSuccess": "修改密碼成功,請重新登錄",
"updateOwnPasswordFailed": "修改密碼失敗",
"deleteSuccess": "刪除用戶成功",
"deleteFailed": "刪除用戶失敗",
"deleteConfirm": "確定要刪除這個用戶嗎?",
"total": "共 {{total}} 條"
},
"statistics": {
"title": "統計信息",
"totalOrders": "總訂單數",
"totalPnl": "總盈虧",
"winRate": "勝率",
"avgPnl": "平均盈虧",
"maxProfit": "最大盈利",
"maxLoss": "最大虧損",
"startDate": "開始日期",
"endDate": "結束日期",
"fetchFailed": "獲取統計信息失敗",
"refresh": "刷新",
"reset": "重置"
},
"orderList": {
"title": "訂單管理",
"leader": "Leader",
"market": "市場",
"category": "分類",
"side": "方向",
"price": "價格",
"size": "數量",
"status": "狀態",
"pnl": "盈虧",
"createdAt": "創建時間",
"fetchFailed": "獲取訂單列表失敗"
},
"templateList": {
"title": "跟單模板管理",
"addTemplate": "新增模板",
"searchPlaceholder": "搜索模板名稱",
"templateName": "模板名稱",
"copyMode": "跟單模式",
"ratio": "比例",
"fixedAmount": "固定金額",
"ratioMode": "比例模式",
"fixedAmountMode": "固定金額模式",
"copyConfig": "跟單配置",
"supportSell": "跟單賣出",
"notSupportSell": "不跟單賣出",
"useCount": "使用次數",
"timesUsed": "次使用",
"amountLimit": "金額限制",
"max": "最大",
"min": "最小",
"notSet": "未設置",
"otherConfig": "其他配置",
"maxDailyOrders": "每日最大訂單",
"priceTolerance": "價格容忍度",
"copy": "複製",
"copySuffix": "副本",
"noData": "暫無模板數據",
"fetchFailed": "獲取模板列表失敗",
"deleteSuccess": "刪除模板成功",
"deleteFailed": "刪除模板失敗",
"deleteConfirm": "確定要刪除這個模板嗎?",
"deleteConfirmDesc": "刪除後無法恢復,請確保沒有跟單關係在使用該模板",
"copySuccess": "複製模板成功",
"copyFailed": "複製模板失敗",
"minAmountError": "最小金額必須 >= 1",
"fixedAmountRequired": "請輸入固定跟單金額",
"invalidNumber": "請輸入有效的數字",
"fixedAmountError": "固定金額必須 >= 1,請重新輸入"
},
"copyTradingList": {
"title": "跟單配置管理",
"addCopyTrading": "新增跟單",
"wallet": "錢包",
"account": "賬戶",
"template": "模板",
"leader": "Leader",
"enabled": "開啟",
"disabled": "停止",
"totalPnl": "總盈虧",
"statistics": "統計",
"orders": "訂單",
"viewStatistics": "查看統計",
"buyOrders": "買入訂單",
"sellOrders": "賣出訂單",
"matchedOrders": "匹配關係",
"filterWallet": "篩選錢包",
"filterTemplate": "篩選模板",
"filterLeader": "篩選 Leader",
"fetchFailed": "獲取跟單列表失敗",
"startSuccess": "開啟跟單成功",
"stopSuccess": "停止跟單成功",
"updateStatusFailed": "更新跟單狀態失敗",
"deleteSuccess": "刪除跟單成功",
"deleteFailed": "刪除跟單失敗",
"deleteConfirm": "確定要刪除這個跟單關係嗎?"
}
}
+3
View File
@@ -1,6 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './i18n/config' // 初始化 i18n
import './styles/index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
@@ -9,3 +10,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
</React.StrictMode>,
)
+50 -44
View File
@@ -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
+43 -41
View File
@@ -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
View File
@@ -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>
+174
View File
@@ -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
+10 -7
View File
@@ -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"
+40 -38
View File
@@ -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}
+127
View File
@@ -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
+34 -32
View File
@@ -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>
+14 -11
View File
@@ -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
+15 -13
View File
@@ -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>
+239
View File
@@ -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
+45 -43
View File
@@ -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>
+14 -12
View File
@@ -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' }}
+59 -57
View File
@@ -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>
+56 -54
View File
@@ -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>
+5 -1
View File
@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosError } from 'axios'
import type { ApiResponse } from '../types'
import { getToken, setToken, removeToken } from '../utils'
import { wsManager } from './websocket'
import i18n from '../i18n/config'
/**
* API
@@ -36,6 +37,9 @@ apiClient.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加语言 Header
const language = i18n.language || 'en'
config.headers['X-Language'] = language
return config
},
(error) => {
@@ -473,5 +477,5 @@ export const apiService = {
}
}
export default apiClient
export default apiService