From 045834dcbe32ed21b218069a0c167cacb12cbb3f Mon Sep 17 00:00:00 2001 From: tangmengqiu <1124090103@qq.com> Date: Mon, 3 Nov 2025 23:15:38 -0500 Subject: [PATCH 01/62] =?UTF-8?q?feat(hyperliquid):=20Auto-generate=20wall?= =?UTF-8?q?et=20address=20from=20private=20key=20Enable=20automatic=20wall?= =?UTF-8?q?et=20address=20generation=20from=20private=20key=20for=20Hyperl?= =?UTF-8?q?iquid=20exchange,=20simplifying=20user=20onboarding=20and=20red?= =?UTF-8?q?ucing=20configuration=20errors.=20Backend=20Changes=20(trader/h?= =?UTF-8?q?yperliquid=5Ftrader.go):=20-=20Import=20crypto/ecdsa=20package?= =?UTF-8?q?=20for=20ECDSA=20public=20key=20operations=20-=20Enable=20walle?= =?UTF-8?q?t=20address=20auto-generation=20when=20walletAddr=20is=20empty?= =?UTF-8?q?=20-=20Use=20crypto.PubkeyToAddress()=20to=20derive=20address?= =?UTF-8?q?=20from=20private=20key=20-=20Add=20logging=20for=20both=20auto?= =?UTF-8?q?-generated=20and=20manually=20provided=20addresses=20Frontend?= =?UTF-8?q?=20Changes=20(web/src/components/AITradersPage.tsx):=20-=20Remo?= =?UTF-8?q?ve=20wallet=20address=20required=20validation=20(only=20private?= =?UTF-8?q?=20key=20required)=20-=20Update=20button=20disabled=20state=20t?= =?UTF-8?q?o=20only=20check=20private=20key=20-=20Add=20"Optional"=20label?= =?UTF-8?q?=20to=20wallet=20address=20field=20-=20Add=20dynamic=20placehol?= =?UTF-8?q?der=20with=20bilingual=20hint=20-=20Show=20context-aware=20help?= =?UTF-8?q?er=20text=20based=20on=20input=20state=20-=20Remove=20HTML=20re?= =?UTF-8?q?quired=20attribute=20from=20input=20field=20Translation=20Updat?= =?UTF-8?q?es=20(web/src/i18n/translations.ts):=20-=20Add=20'optional'=20t?= =?UTF-8?q?ranslation=20(EN:=20"Optional",=20ZH:=20"=E5=8F=AF=E9=80=89")?= =?UTF-8?q?=20-=20Add=20'hyperliquidWalletAddressAutoGenerate'=20translati?= =?UTF-8?q?on=20=20=20EN:=20"Leave=20blank=20to=20automatically=20generate?= =?UTF-8?q?=20wallet=20address=20from=20private=20key"=20=20=20ZH:=20"?= =?UTF-8?q?=E7=95=99=E7=A9=BA=E5=B0=86=E8=87=AA=E5=8A=A8=E4=BB=8E=E7=A7=81?= =?UTF-8?q?=E9=92=A5=E7=94=9F=E6=88=90=E9=92=B1=E5=8C=85=E5=9C=B0=E5=9D=80?= =?UTF-8?q?"=20Benefits:=20=E2=9C=85=20Simplified=20UX=20-=20Users=20only?= =?UTF-8?q?=20need=20to=20provide=20private=20key=20=E2=9C=85=20Error=20pr?= =?UTF-8?q?evention=20-=20Auto-generated=20address=20always=20matches=20pr?= =?UTF-8?q?ivate=20key=20=E2=9C=85=20Backward=20compatible=20-=20Manual=20?= =?UTF-8?q?address=20input=20still=20supported=20=E2=9C=85=20Better=20UX?= =?UTF-8?q?=20-=20Clear=20visual=20indicators=20for=20optional=20fields=20?= =?UTF-8?q?Technical=20Details:=20-=20Uses=20Ethereum=20standard=20ECDSA?= =?UTF-8?q?=20public=20key=20to=20address=20conversion=20-=20Implementatio?= =?UTF-8?q?n=20was=20already=20present=20but=20commented=20out=20(lines=20?= =?UTF-8?q?37-43)=20-=20No=20database=20schema=20changes=20required=20(hyp?= =?UTF-8?q?erliquid=5Fwallet=5Faddr=20already=20nullable)=20-=20Fallback?= =?UTF-8?q?=20behavior:=20manual=20input=20>=20auto-generation=20Co-Author?= =?UTF-8?q?ed-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/hyperliquid_trader.go | 20 +++++++++++++------- web/src/components/AITradersPage.tsx | 16 ++++++++++------ web/src/i18n/translations.ts | 4 ++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..0c7684d3 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,6 +2,7 @@ package trader import ( "context" + "crypto/ecdsa" "encoding/json" "fmt" "log" @@ -34,13 +35,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) apiURL = hyperliquid.TestnetAPIURL } - // // 从私钥生成钱包地址 - // pubKey := privateKey.Public() - // publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) - // if !ok { - // return nil, fmt.Errorf("无法转换公钥") - // } - // walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + // 从私钥生成钱包地址(如果未提供) + if walletAddr == "" { + pubKey := privateKey.Public() + publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("无法转换公钥") + } + walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr) + } else { + log.Printf("✓ 使用提供的钱包地址: %s", walletAddr) + } ctx := context.Background() diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 41b3cdc2..f3364551 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -1201,7 +1201,7 @@ function ExchangeConfigModal({ if (!apiKey.trim() || !secretKey.trim()) return; await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet); } else if (selectedExchange?.id === 'hyperliquid') { - if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return; + if (!apiKey.trim()) return; // 只验证私钥,钱包地址可选(会自动生成) await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim()); } else if (selectedExchange?.id === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return; @@ -1360,18 +1360,22 @@ function ExchangeConfigModal({
setHyperliquidWalletAddr(e.target.value)} - placeholder={t('enterWalletAddress', language)} + placeholder="0x... (留空将自动从私钥生成 / Leave blank to auto-generate)" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - required />
- {t('hyperliquidWalletAddressDesc', language)} + {hyperliquidWalletAddr.trim() + ? t('hyperliquidWalletAddressDesc', language) + : t('hyperliquidWalletAddressAutoGenerate', language)}
@@ -1468,10 +1472,10 @@ function ExchangeConfigModal({ {/* Chart */} -
+
{/* NOFX Watermark */} -
NOFX
- + - - - + + + - + } /> 50 ? false : { fill: '#F0B90B', r: 3 }} activeDot={{ @@ -352,72 +372,72 @@ export function EquityChart({ traderId }: EquityChartProps) { {/* Footer Stats */}
{t('initialBalance', language)}
{initialBalance.toFixed(2)} USDT
{t('currentEquity', language)}
{currentValue.raw_equity.toFixed(2)} USDT
{t('historicalCycles', language)}
{validHistory.length} {t('cycles', language)}
{t('displayRange', language)}
{validHistory.length > MAX_DISPLAY_POINTS diff --git a/web/src/components/ExchangeIcons.tsx b/web/src/components/ExchangeIcons.tsx index c3056dd0..0ffa695c 100644 --- a/web/src/components/ExchangeIcons.tsx +++ b/web/src/components/ExchangeIcons.tsx @@ -1,107 +1,165 @@ -import React from 'react'; +import React from 'react' interface IconProps { - width?: number; - height?: number; - className?: string; + width?: number + height?: number + className?: string } // Binance SVG 图标组件 -const BinanceIcon: React.FC = ({ width = 24, height = 24, className }) => ( - = ({ + width = 24, + height = 24, + className, +}) => ( + - -); +) // Hyperliquid SVG 图标组件 -const HyperliquidIcon: React.FC = ({ width = 24, height = 24, className }) => ( - = ({ + width = 24, + height = 24, + className, +}) => ( + - -); +) // Aster SVG 图标组件 -const AsterIcon: React.FC = ({ width = 24, height = 24, className }) => ( - = ({ + width = 24, + height = 24, + className, +}) => ( + - - - + + + - - - + + + - - - + + + - - + + - - - - + + + + -); +) // 获取交易所图标的函数 -export const getExchangeIcon = (exchangeType: string, props: IconProps = {}) => { +export const getExchangeIcon = ( + exchangeType: string, + props: IconProps = {} +) => { // 支持完整ID或类型名 - const type = exchangeType.toLowerCase().includes('binance') ? 'binance' : - exchangeType.toLowerCase().includes('hyperliquid') ? 'hyperliquid' : - exchangeType.toLowerCase().includes('aster') ? 'aster' : - exchangeType.toLowerCase(); - + const type = exchangeType.toLowerCase().includes('binance') + ? 'binance' + : exchangeType.toLowerCase().includes('hyperliquid') + ? 'hyperliquid' + : exchangeType.toLowerCase().includes('aster') + ? 'aster' + : exchangeType.toLowerCase() + const iconProps = { width: props.width || 24, height: props.height || 24, - className: props.className - }; - + className: props.className, + } + switch (type) { case 'binance': case 'cex': - return ; + return case 'hyperliquid': case 'dex': - return ; + return case 'aster': - return ; + return default: return ( -
justifyContent: 'center', fontSize: '12px', fontWeight: 'bold', - color: '#EAECEF' + color: '#EAECEF', }} > {type[0]?.toUpperCase() || '?'}
- ); + ) } -}; \ No newline at end of file +} diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 06352dee..e39731c1 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,12 +1,12 @@ -import { useLanguage } from '../contexts/LanguageContext'; -import { t } from '../i18n/translations'; +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' interface HeaderProps { - simple?: boolean; // For login/register pages + simple?: boolean // For login/register pages } export function Header({ simple = false }: HeaderProps) { - const { language, setLanguage } = useLanguage(); + const { language, setLanguage } = useLanguage() return (
@@ -28,15 +28,19 @@ export function Header({ simple = false }: HeaderProps) { )}
- + {/* Right - Language Toggle (always show) */} -
+
- ); + ) } diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index a1ed3512..d73d078b 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -1,208 +1,272 @@ -import React, { useState } from 'react'; -import { useAuth } from '../contexts/AuthContext'; -import { useLanguage } from '../contexts/LanguageContext'; -import { t } from '../i18n/translations'; -import HeaderBar from './landing/HeaderBar'; +import React, { useState } from 'react' +import { useAuth } from '../contexts/AuthContext' +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' +import HeaderBar from './landing/HeaderBar' export function LoginPage() { - const { language } = useLanguage(); - const { login, verifyOTP } = useAuth(); - const [step, setStep] = useState<'login' | 'otp'>('login'); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [otpCode, setOtpCode] = useState(''); - const [userID, setUserID] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); + const { language } = useLanguage() + const { login, verifyOTP } = useAuth() + const [step, setStep] = useState<'login' | 'otp'>('login') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [otpCode, setOtpCode] = useState('') + const [userID, setUserID] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) const handleLogin = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); + e.preventDefault() + setError('') + setLoading(true) + + const result = await login(email, password) - const result = await login(email, password); - if (result.success) { if (result.requiresOTP && result.userID) { - setUserID(result.userID); - setStep('otp'); + setUserID(result.userID) + setStep('otp') } } else { - setError(result.message || t('loginFailed', language)); + setError(result.message || t('loginFailed', language)) } - - setLoading(false); - }; + + setLoading(false) + } const handleOTPVerify = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); + e.preventDefault() + setError('') + setLoading(true) + + const result = await verifyOTP(userID, otpCode) - const result = await verifyOTP(userID, otpCode); - if (!result.success) { - setError(result.message || t('verificationFailed', language)); + setError(result.message || t('verificationFailed', language)) } // 成功的话AuthContext会自动处理登录状态 - - setLoading(false); - }; + + setLoading(false) + } return (
- {}} - isLoggedIn={false} + {}} + isLoggedIn={false} isHomePage={false} currentPage="login" language={language} onLanguageChange={() => {}} onPageChange={(page) => { - console.log('LoginPage onPageChange called with:', page); + console.log('LoginPage onPageChange called with:', page) if (page === 'competition') { - window.location.href = '/competition'; + window.location.href = '/competition' } }} /> -
+
- {/* Logo */}
- NoFx Logo + NoFx Logo
-

+

登录 NOFX

-

+

{step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}

- {/* Login Form */} -
- {step === 'login' ? ( - -
- - setEmail(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('emailPlaceholder', language)} - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('passwordPlaceholder', language)} - required - /> -
- - {error && ( -
- {error} + {/* Login Form */} +
+ {step === 'login' ? ( + +
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 rounded" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('emailPlaceholder', language)} + required + />
- )} - - - ) : ( -
-
-
📱
-

- {t('scanQRCodeInstructions', language)}
- {t('enterOTPCode', language)} -

-
- -
- - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 rounded" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('passwordPlaceholder', language)} + required + />
- )} -
- + {error && ( +
+ {error} +
+ )} + -
- - )} -
+ + ) : ( +
+
+
📱
+

+ {t('scanQRCodeInstructions', language)} +
+ {t('enterOTPCode', language)} +

+
- {/* Register Link */} -
-

- 还没有账户?{' '} - -

+
+ + + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full px-3 py-2 rounded text-center text-2xl font-mono" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('otpPlaceholder', language)} + maxLength={6} + required + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+ + )} +
+ + {/* Register Link */} +
+

+ 还没有账户?{' '} + +

+
-
- ); + ) } diff --git a/web/src/components/ModelIcons.tsx b/web/src/components/ModelIcons.tsx index c9cb1ff8..78dbd418 100644 --- a/web/src/components/ModelIcons.tsx +++ b/web/src/components/ModelIcons.tsx @@ -1,26 +1,25 @@ - interface IconProps { - width?: number; - height?: number; - className?: string; + width?: number + height?: number + className?: string } // 获取AI模型图标的函数 export const getModelIcon = (modelType: string, props: IconProps = {}) => { // 支持完整ID或类型名 - const type = modelType.includes('_') ? modelType.split('_').pop() : modelType; - - let iconPath: string | null = null; - + const type = modelType.includes('_') ? modelType.split('_').pop() : modelType + + let iconPath: string | null = null + switch (type) { case 'deepseek': - iconPath = '/icons/deepseek.svg'; - break; + iconPath = '/icons/deepseek.svg' + break case 'qwen': - iconPath = '/icons/qwen.svg'; - break; + iconPath = '/icons/qwen.svg' + break default: - return null; + return null } return ( @@ -32,5 +31,5 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => { className={props.className} style={{ borderRadius: '50%' }} /> - ); -}; \ No newline at end of file + ) +} diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 4fdbcace..3773714b 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -1,366 +1,502 @@ -import React, { useState, useEffect } from 'react'; -import { useAuth } from '../contexts/AuthContext'; -import { useLanguage } from '../contexts/LanguageContext'; -import { t } from '../i18n/translations'; -import { getSystemConfig } from '../lib/config'; -import HeaderBar from './landing/HeaderBar'; +import React, { useState, useEffect } from 'react' +import { useAuth } from '../contexts/AuthContext' +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' +import { getSystemConfig } from '../lib/config' +import HeaderBar from './landing/HeaderBar' export function RegisterPage() { - const { language } = useLanguage(); - const { register, completeRegistration } = useAuth(); - const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp'>('register'); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [betaCode, setBetaCode] = useState(''); - const [betaMode, setBetaMode] = useState(false); - const [otpCode, setOtpCode] = useState(''); - const [userID, setUserID] = useState(''); - const [otpSecret, setOtpSecret] = useState(''); - const [qrCodeURL, setQrCodeURL] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); + const { language } = useLanguage() + const { register, completeRegistration } = useAuth() + const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp'>( + 'register' + ) + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [betaCode, setBetaCode] = useState('') + const [betaMode, setBetaMode] = useState(false) + const [otpCode, setOtpCode] = useState('') + const [userID, setUserID] = useState('') + const [otpSecret, setOtpSecret] = useState('') + const [qrCodeURL, setQrCodeURL] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) useEffect(() => { // 获取系统配置,检查是否开启内测模式 - getSystemConfig().then(config => { - setBetaMode(config.beta_mode || false); - }).catch(err => { - console.error('Failed to fetch system config:', err); - }); - }, []); + getSystemConfig() + .then((config) => { + setBetaMode(config.beta_mode || false) + }) + .catch((err) => { + console.error('Failed to fetch system config:', err) + }) + }, []) const handleRegister = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); + e.preventDefault() + setError('') if (password !== confirmPassword) { - setError(t('passwordMismatch', language)); - return; + setError(t('passwordMismatch', language)) + return } if (password.length < 6) { - setError(t('passwordTooShort', language)); - return; + setError(t('passwordTooShort', language)) + return } if (betaMode && !betaCode.trim()) { - setError('内测期间,注册需要提供内测码'); - return; + setError('内测期间,注册需要提供内测码') + return } - setLoading(true); + setLoading(true) + + const result = await register(email, password, betaCode.trim() || undefined) - const result = await register(email, password, betaCode.trim() || undefined); - if (result.success && result.userID) { - setUserID(result.userID); - setOtpSecret(result.otpSecret || ''); - setQrCodeURL(result.qrCodeURL || ''); - setStep('setup-otp'); + setUserID(result.userID) + setOtpSecret(result.otpSecret || '') + setQrCodeURL(result.qrCodeURL || '') + setStep('setup-otp') } else { - setError(result.message || t('registrationFailed', language)); + setError(result.message || t('registrationFailed', language)) } - - setLoading(false); - }; + + setLoading(false) + } const handleSetupComplete = () => { - setStep('verify-otp'); - }; + setStep('verify-otp') + } const handleOTPVerify = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); + e.preventDefault() + setError('') + setLoading(true) + + const result = await completeRegistration(userID, otpCode) - const result = await completeRegistration(userID, otpCode); - if (!result.success) { - setError(result.message || t('registrationFailed', language)); + setError(result.message || t('registrationFailed', language)) } // 成功的话AuthContext会自动处理登录状态 - - setLoading(false); - }; + + setLoading(false) + } const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - }; + navigator.clipboard.writeText(text) + } return (
- {}} onPageChange={(page) => { - console.log('RegisterPage onPageChange called with:', page); + console.log('RegisterPage onPageChange called with:', page) if (page === 'competition') { - window.location.href = '/competition'; + window.location.href = '/competition' } }} /> -
+
- {/* Logo */}
-
- NoFx Logo +
+ NoFx Logo +
+

+ {t('appTitle', language)} +

+

+ {step === 'register' && t('registerTitle', language)} + {step === 'setup-otp' && t('setupTwoFactor', language)} + {step === 'verify-otp' && t('verifyOTP', language)} +

-

- {t('appTitle', language)} -

-

- {step === 'register' && t('registerTitle', language)} - {step === 'setup-otp' && t('setupTwoFactor', language)} - {step === 'verify-otp' && t('verifyOTP', language)} -

-
- {/* Registration Form */} -
- {step === 'register' && ( -
-
- - setEmail(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('emailPlaceholder', language)} - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('passwordPlaceholder', language)} - required - /> -
- -
- - setConfirmPassword(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('confirmPasswordPlaceholder', language)} - required - /> -
- - {betaMode && ( + {/* Registration Form */} +
+ {step === 'register' && ( +
-
- )} - {error && ( -
- {error} +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 rounded" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('passwordPlaceholder', language)} + required + />
- )} - - - )} +
+ + setConfirmPassword(e.target.value)} + className="w-full px-3 py-2 rounded" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('confirmPasswordPlaceholder', language)} + required + /> +
- {step === 'setup-otp' && ( -
-
-
📱
-

- {t('setupTwoFactor', language)} -

-

- {t('setupTwoFactorDesc', language)} -

-
+ {betaMode && ( +
+ + + setBetaCode( + e.target.value + .replace(/[^a-z0-9]/gi, '') + .toLowerCase() + ) + } + className="w-full px-3 py-2 rounded font-mono" + style={{ + background: '#0B0E11', + border: '1px solid #2B3139', + color: '#EAECEF', + }} + placeholder="请输入6位内测码" + maxLength={6} + required={betaMode} + /> +

+ 内测码由6位字母数字组成,区分大小写 +

+
+ )} -
-
-

- {t('authStep1Title', language)} -

-

- {t('authStep1Desc', language)} + {error && ( +

+ {error} +
+ )} + + + + )} + + {step === 'setup-otp' && ( +
+
+
📱
+

+ {t('setupTwoFactor', language)} +

+

+ {t('setupTwoFactorDesc', language)}

-
-

- {t('authStep2Title', language)} -

-

- {t('authStep2Desc', language)} -

- - {qrCodeURL && ( +
+
+

+ {t('authStep1Title', language)} +

+

+ {t('authStep1Desc', language)} +

+
+ +
+

+ {t('authStep2Title', language)} +

+

+ {t('authStep2Desc', language)} +

+ + {qrCodeURL && ( +
+

+ {t('qrCodeHint', language)} +

+
+ QR Code +
+
+ )} +
-

{t('qrCodeHint', language)}

-
- QR Code +

+ {t('otpSecret', language)} +

+
+ + {otpSecret} + +
- )} - -
-

{t('otpSecret', language)}

-
- - {otpSecret} - - -
+
+ +
+

+ {t('authStep3Title', language)} +

+

+ {t('authStep3Desc', language)} +

-
-

- {t('authStep3Title', language)} -

-

- {t('authStep3Desc', language)} -

-
-
- - -
- )} - - {step === 'verify-otp' && ( -
-
-
🔐
-

- {t('enterOTPCode', language)}
- {t('completeRegistrationSubtitle', language)} -

-
- -
- - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} -
- )} - -
-
-
- )} -
+ )} - {/* Login Link */} - {step === 'register' && ( -
-

- 已有账户?{' '} - -

+ {step === 'verify-otp' && ( +
+
+
🔐
+

+ {t('enterOTPCode', language)} +
+ {t('completeRegistrationSubtitle', language)} +

+
+ +
+ + + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full px-3 py-2 rounded text-center text-2xl font-mono" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('otpPlaceholder', language)} + maxLength={6} + required + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ )}
- )} + + {/* Login Link */} + {step === 'register' && ( +
+

+ 已有账户?{' '} + +

+
+ )}
- ); + ) } diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 4676c194..f96c9070 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -1,52 +1,52 @@ -import { useState, useEffect } from 'react'; -import type { AIModel, Exchange, CreateTraderRequest } from '../types'; -import { useLanguage } from '../contexts/LanguageContext'; -import { t } from '../i18n/translations'; +import { useState, useEffect } from 'react' +import type { AIModel, Exchange, CreateTraderRequest } from '../types' +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' // 提取下划线后面的名称部分 function getShortName(fullName: string): string { - const parts = fullName.split('_'); - return parts.length > 1 ? parts[parts.length - 1] : fullName; + const parts = fullName.split('_') + return parts.length > 1 ? parts[parts.length - 1] : fullName } interface TraderConfigData { - trader_id?: string; - trader_name: string; - ai_model: string; - exchange_id: string; - btc_eth_leverage: number; - altcoin_leverage: number; - trading_symbols: string; - custom_prompt: string; - override_base_prompt: boolean; - system_prompt_template: string; - is_cross_margin: boolean; - use_coin_pool: boolean; - use_oi_top: boolean; - initial_balance: number; - scan_interval_minutes: number; + trader_id?: string + trader_name: string + ai_model: string + exchange_id: string + btc_eth_leverage: number + altcoin_leverage: number + trading_symbols: string + custom_prompt: string + override_base_prompt: boolean + system_prompt_template: string + is_cross_margin: boolean + use_coin_pool: boolean + use_oi_top: boolean + initial_balance: number + scan_interval_minutes: number } interface TraderConfigModalProps { - isOpen: boolean; - onClose: () => void; - traderData?: TraderConfigData | null; - isEditMode?: boolean; - availableModels?: AIModel[]; - availableExchanges?: Exchange[]; - onSave?: (data: CreateTraderRequest) => Promise; + isOpen: boolean + onClose: () => void + traderData?: TraderConfigData | null + isEditMode?: boolean + availableModels?: AIModel[] + availableExchanges?: Exchange[] + onSave?: (data: CreateTraderRequest) => Promise } -export function TraderConfigModal({ - isOpen, - onClose, - traderData, +export function TraderConfigModal({ + isOpen, + onClose, + traderData, isEditMode = false, availableModels = [], availableExchanges = [], - onSave + onSave, }: TraderConfigModalProps) { - const { language } = useLanguage(); + const { language } = useLanguage() const [formData, setFormData] = useState({ trader_name: '', ai_model: '', @@ -62,20 +62,23 @@ export function TraderConfigModal({ use_oi_top: false, initial_balance: 1000, scan_interval_minutes: 3, - }); - const [isSaving, setIsSaving] = useState(false); - const [availableCoins, setAvailableCoins] = useState([]); - const [selectedCoins, setSelectedCoins] = useState([]); - const [showCoinSelector, setShowCoinSelector] = useState(false); - const [promptTemplates, setPromptTemplates] = useState<{name: string}[]>([]); + }) + const [isSaving, setIsSaving] = useState(false) + const [availableCoins, setAvailableCoins] = useState([]) + const [selectedCoins, setSelectedCoins] = useState([]) + const [showCoinSelector, setShowCoinSelector] = useState(false) + const [promptTemplates, setPromptTemplates] = useState<{ name: string }[]>([]) useEffect(() => { if (traderData) { - setFormData(traderData); + setFormData(traderData) // 设置已选择的币种 if (traderData.trading_symbols) { - const coins = traderData.trading_symbols.split(',').map(s => s.trim()).filter(s => s); - setSelectedCoins(coins); + const coins = traderData.trading_symbols + .split(',') + .map((s) => s.trim()) + .filter((s) => s) + setSelectedCoins(coins) } } else if (!isEditMode) { setFormData({ @@ -93,85 +96,96 @@ export function TraderConfigModal({ use_oi_top: false, initial_balance: 1000, scan_interval_minutes: 3, - }); + }) } // 确保旧数据也有默认的 system_prompt_template if (traderData && !traderData.system_prompt_template) { - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - system_prompt_template: 'default' - })); + system_prompt_template: 'default', + })) } - }, [traderData, isEditMode, availableModels, availableExchanges]); + }, [traderData, isEditMode, availableModels, availableExchanges]) // 获取系统配置中的币种列表 useEffect(() => { const fetchConfig = async () => { try { - const response = await fetch('/api/config'); - const config = await response.json(); + const response = await fetch('/api/config') + const config = await response.json() if (config.default_coins) { - setAvailableCoins(config.default_coins); + setAvailableCoins(config.default_coins) } } catch (error) { - console.error('Failed to fetch config:', error); + console.error('Failed to fetch config:', error) // 使用默认币种列表 - setAvailableCoins(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT', 'DOGEUSDT', 'ADAUSDT']); + setAvailableCoins([ + 'BTCUSDT', + 'ETHUSDT', + 'SOLUSDT', + 'BNBUSDT', + 'XRPUSDT', + 'DOGEUSDT', + 'ADAUSDT', + ]) } - }; - fetchConfig(); - }, []); + } + fetchConfig() + }, []) // 获取系统提示词模板列表 useEffect(() => { const fetchPromptTemplates = async () => { try { - const response = await fetch('/api/prompt-templates'); - const data = await response.json(); + const response = await fetch('/api/prompt-templates') + const data = await response.json() if (data.templates) { - setPromptTemplates(data.templates); + setPromptTemplates(data.templates) } } catch (error) { - console.error('Failed to fetch prompt templates:', error); + console.error('Failed to fetch prompt templates:', error) // 使用默认模板列表 - setPromptTemplates([{name: 'default'}, {name: 'aggressive'}]); + setPromptTemplates([{ name: 'default' }, { name: 'aggressive' }]) } - }; - fetchPromptTemplates(); - }, []); + } + fetchPromptTemplates() + }, []) // 当选择的币种改变时,更新输入框 useEffect(() => { - const symbolsString = selectedCoins.join(','); - setFormData(prev => ({ ...prev, trading_symbols: symbolsString })); - }, [selectedCoins]); + const symbolsString = selectedCoins.join(',') + setFormData((prev) => ({ ...prev, trading_symbols: symbolsString })) + }, [selectedCoins]) - if (!isOpen) return null; + if (!isOpen) return null const handleInputChange = (field: keyof TraderConfigData, value: any) => { - setFormData(prev => ({ ...prev, [field]: value })); - + setFormData((prev) => ({ ...prev, [field]: value })) + // 如果是直接编辑trading_symbols,同步更新selectedCoins if (field === 'trading_symbols') { - const coins = value.split(',').map((s: string) => s.trim()).filter((s: string) => s); - setSelectedCoins(coins); + const coins = value + .split(',') + .map((s: string) => s.trim()) + .filter((s: string) => s) + setSelectedCoins(coins) } - }; + } const handleCoinToggle = (coin: string) => { - setSelectedCoins(prev => { + setSelectedCoins((prev) => { if (prev.includes(coin)) { - return prev.filter(c => c !== coin); + return prev.filter((c) => c !== coin) } else { - return [...prev, coin]; + return [...prev, coin] } - }); - }; + }) + } const handleSave = async () => { - if (!onSave) return; + if (!onSave) return - setIsSaving(true); + setIsSaving(true) try { const saveData: CreateTraderRequest = { name: formData.trader_name, @@ -188,19 +202,19 @@ export function TraderConfigModal({ use_oi_top: formData.use_oi_top, initial_balance: formData.initial_balance, scan_interval_minutes: formData.scan_interval_minutes, - }; - await onSave(saveData); - onClose(); + } + await onSave(saveData) + onClose() } catch (error) { - console.error('保存失败:', error); + console.error('保存失败:', error) } finally { - setIsSaving(false); + setIsSaving(false) } - }; + } return (
-
e.stopPropagation()} > @@ -236,24 +250,32 @@ export function TraderConfigModal({
- + handleInputChange('trader_name', e.target.value)} + onChange={(e) => + handleInputChange('trader_name', e.target.value) + } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" placeholder="请输入交易员名称" />
- +
- + @@ -287,14 +315,16 @@ export function TraderConfigModal({ {/* 第一行:保证金模式和初始余额 */}
- +
- + handleInputChange('initial_balance', Number(e.target.value))} + onChange={(e) => + handleInputChange( + 'initial_balance', + Number(e.target.value) + ) + } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="100" step="100" @@ -329,17 +368,26 @@ export function TraderConfigModal({ {/* 第二行:AI 扫描决策间隔 */}
- + handleInputChange('scan_interval_minutes', Number(e.target.value))} + onChange={(e) => + handleInputChange( + 'scan_interval_minutes', + Number(e.target.value) + ) + } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="1" max="60" step="1" /> -

{t('scanIntervalRecommend', language)}

+

+ {t('scanIntervalRecommend', language)} +

@@ -347,22 +395,36 @@ export function TraderConfigModal({ {/* 第三行:杠杆设置 */}
- + handleInputChange('btc_eth_leverage', Number(e.target.value))} + onChange={(e) => + handleInputChange( + 'btc_eth_leverage', + Number(e.target.value) + ) + } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="1" max="125" />
- + handleInputChange('altcoin_leverage', Number(e.target.value))} + onChange={(e) => + handleInputChange( + 'altcoin_leverage', + Number(e.target.value) + ) + } className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" min="1" max="75" @@ -373,7 +435,9 @@ export function TraderConfigModal({ {/* 第三行:交易币种 */}
- +
handleInputChange('use_oi_top', e.target.checked)} + onChange={(e) => + handleInputChange('use_oi_top', e.target.checked) + } className="w-4 h-4" /> - +
@@ -451,17 +527,24 @@ export function TraderConfigModal({
{/* 系统提示词模板选择 */}
- + @@ -474,21 +557,47 @@ export function TraderConfigModal({ handleInputChange('override_base_prompt', e.target.checked)} + onChange={(e) => + handleInputChange('override_base_prompt', e.target.checked) + } className="w-4 h-4" /> - 启用后将完全替换默认策略 + + + + + + {' '} + 启用后将完全替换默认策略 +